From 6aa324976a6d0eea64b56595dca34af158867328 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Sun, 15 Mar 2026 23:52:04 +0100 Subject: [PATCH 01/34] init --- .context/README.md | 6 +- apps/backend/package.json | 6 - apps/backend/scripts/test-mock-data.ts | 2 +- apps/backend/src/http.ts | 4 +- apps/backend/src/index.ts | 19 +- apps/backend/src/lib/env-vars.ts | 5 +- apps/backend/src/lib/policy-utils.ts | 2 +- .../backend/src/policies/attachment-policy.ts | 7 +- apps/backend/src/policies/bot-policy.ts | 7 +- .../src/policies/channel-member-policy.ts | 7 +- apps/backend/src/policies/channel-policy.ts | 7 +- .../src/policies/channel-section-policy.ts | 5 +- .../src/policies/channel-webhook-policy.ts | 5 +- .../src/policies/custom-emoji-policy.ts | 7 +- .../policies/github-subscription-policy.ts | 5 +- .../policies/integration-connection-policy.ts | 5 +- .../backend/src/policies/invitation-policy.ts | 7 +- apps/backend/src/policies/message-policy.ts | 7 +- .../src/policies/message-reaction-policy.ts | 5 +- .../src/policies/notification-policy.ts | 7 +- .../policies/organization-member-policy.ts | 5 +- .../src/policies/organization-policy.ts | 7 +- .../src/policies/pinned-message-policy.ts | 7 +- .../src/policies/policy-test-helpers.ts | 2 +- .../src/policies/rss-subscription-policy.ts | 5 +- .../src/policies/typing-indicator-policy.ts | 5 +- apps/backend/src/policies/user-policy.ts | 7 +- .../policies/user-presence-status-policy.ts | 5 +- .../src/routes/api-v1/messages.http.ts | 5 +- apps/backend/src/routes/auth.http.ts | 3 +- apps/backend/src/routes/bot-commands.http.ts | 9 +- apps/backend/src/routes/chat-sync.http.ts | 2 +- .../src/routes/incoming-webhooks.http.ts | 2 +- .../src/routes/integration-commands.http.ts | 2 +- .../src/routes/integration-resources.http.ts | 2 +- apps/backend/src/routes/integrations.http.ts | 8 +- apps/backend/src/routes/internal.http.ts | 3 +- apps/backend/src/routes/klipy.http.ts | 3 +- apps/backend/src/routes/mock-data.http.ts | 2 +- apps/backend/src/routes/presence.http.ts | 2 +- apps/backend/src/routes/root.http.ts | 2 +- apps/backend/src/routes/uploads.http.ts | 2 +- apps/backend/src/routes/webhooks.http.ts | 3 +- apps/backend/src/rpc/handlers/channels.ts | 2 +- .../src/rpc/handlers/connect-shares.ts | 2 +- apps/backend/src/rpc/handlers/invitations.ts | 2 +- apps/backend/src/rpc/middleware/auth-class.ts | 2 +- apps/backend/src/rpc/middleware/auth.test.ts | 4 +- apps/backend/src/rpc/middleware/auth.ts | 2 +- apps/backend/src/rpc/server.ts | 2 +- .../src/services/bot-gateway-service.ts | 9 +- .../src/services/channel-access-sync.ts | 5 +- .../chat-sync-attribution-reconciler.ts | 5 +- .../chat-sync/chat-sync-core-worker.ts | 25 +- .../chat-sync/chat-sync-provider-registry.ts | 11 +- .../chat-sync/discord-gateway-service.ts | 25 +- .../services/chat-sync/discord-sync-worker.ts | 7 +- .../services/connect-conversation-service.ts | 5 +- .../src/services/integration-encryption.ts | 11 +- .../src/services/integration-token-service.ts | 11 +- .../integrations/integration-bot-service.ts | 7 +- .../message-outbox-dispatcher.test.ts | 2 +- .../src/services/message-outbox-dispatcher.ts | 9 +- .../message-side-effect-service.test.ts | 4 +- .../services/message-side-effect-service.ts | 17 +- .../src/services/mock-data-generator.ts | 6 +- .../src/services/oauth/oauth-http-client.ts | 11 +- .../services/oauth/oauth-provider-registry.ts | 7 +- .../src/services/oauth/oauth-provider.ts | 8 +- .../backend/src/services/org-resolver.test.ts | 2 +- apps/backend/src/services/org-resolver.ts | 7 +- apps/backend/src/services/rate-limiter.ts | 8 +- apps/backend/src/services/session-manager.ts | 7 +- .../src/services/webhook-bot-service.ts | 7 +- apps/backend/src/services/workos-auth.ts | 9 +- apps/backend/src/services/workos-webhook.ts | 11 +- apps/bot-gateway/package.json | 1 - apps/bot-gateway/src/index.ts | 27 +- apps/cluster/package.json | 5 - .../cluster/src/cron/presence-cleanup-cron.ts | 2 +- apps/cluster/src/cron/rss-poll-cron.ts | 8 +- .../src/cron/status-expiration-cron.ts | 2 +- .../src/cron/typing-indicator-cleanup-cron.ts | 2 +- apps/cluster/src/cron/upload-cleanup-cron.ts | 2 +- apps/cluster/src/cron/workos-sync-cron.ts | 2 +- apps/cluster/src/index.ts | 7 +- apps/cluster/src/services/bot-user-service.ts | 7 +- .../src/services/openrouter-service.ts | 2 +- .../src/workflows/cleanup-uploads-handler.ts | 2 +- .../workflows/github-installation-handler.ts | 2 +- .../src/workflows/github-webhook-handler.ts | 2 +- .../workflows/message-notification-handler.ts | 2 +- .../src/workflows/rss-feed-poll-handler.ts | 4 +- .../src/workflows/thread-naming-handler.ts | 4 +- apps/electric-proxy/package.json | 2 - apps/electric-proxy/src/auth/bot-auth.ts | 2 +- .../src/cache/access-context-cache.ts | 2 +- .../src/cache/access-context-service.ts | 7 +- .../src/cache/redis-persistence.ts | 2 +- apps/electric-proxy/src/config.ts | 7 +- apps/electric-proxy/src/index.ts | 4 +- .../src/proxy/electric-client.ts | 2 +- apps/electric-proxy/src/tables/bot-tables.ts | 2 +- apps/electric-proxy/src/tables/user-tables.ts | 2 +- apps/link-preview-worker/package.json | 2 - apps/link-preview-worker/src/api.ts | 2 +- apps/link-preview-worker/src/cache.ts | 7 +- apps/link-preview-worker/src/declare.ts | 14 +- apps/link-preview-worker/src/handle.ts | 2 +- .../src/handlers/link-preview.ts | 9 +- .../link-preview-worker/src/handlers/tweet.ts | 6 +- apps/link-preview-worker/src/index.ts | 3 +- .../src/services/twitter.ts | 12 +- apps/web/package.json | 6 +- apps/web/src/atoms/chat-atoms.ts | 2 +- apps/web/src/atoms/chat-query-atoms.ts | 2 +- apps/web/src/atoms/command-palette-state.ts | 2 +- apps/web/src/atoms/custom-emoji-atoms.ts | 2 +- apps/web/src/atoms/desktop-auth.ts | 12 +- apps/web/src/atoms/desktop-callback-atoms.ts | 4 +- apps/web/src/atoms/emoji-atoms.ts | 2 +- apps/web/src/atoms/feature-discovery-atoms.ts | 2 +- apps/web/src/atoms/hotkey-atoms.ts | 2 +- apps/web/src/atoms/loading-state-atoms.ts | 2 +- apps/web/src/atoms/message-atoms.ts | 2 +- apps/web/src/atoms/modal-atoms.ts | 2 +- .../web/src/atoms/notification-sound-atoms.ts | 2 +- apps/web/src/atoms/onboarding-atoms.ts | 2 +- apps/web/src/atoms/organization-atoms.ts | 2 +- apps/web/src/atoms/panel-atoms.ts | 2 +- apps/web/src/atoms/presence-atoms.ts | 2 +- apps/web/src/atoms/react-scan-atoms.ts | 2 +- apps/web/src/atoms/recent-channels-atom.ts | 2 +- apps/web/src/atoms/search-atoms.ts | 2 +- apps/web/src/atoms/section-collapse-atoms.ts | 2 +- apps/web/src/atoms/sidebar-atoms.ts | 2 +- apps/web/src/atoms/tauri-update-atoms.ts | 2 +- apps/web/src/atoms/web-auth.ts | 8 +- apps/web/src/atoms/web-callback-atoms.ts | 4 +- apps/web/src/components/bots/bot-avatar.tsx | 2 +- apps/web/src/components/bots/bot-card.tsx | 2 +- .../add-github-repo-modal.tsx | 2 +- .../channel-settings/add-rss-feed-modal.tsx | 2 +- .../channel-settings/create-webhook-form.tsx | 2 +- .../github-integration-card.tsx | 2 +- .../github-subscription-card.tsx | 2 +- .../channel-settings/integration-card.tsx | 2 +- .../channel-settings/openstatus-section.tsx | 2 +- .../channel-settings/railway-section.tsx | 2 +- .../channel-settings/rss-integration-card.tsx | 2 +- .../chat-sync/add-channel-link-modal.tsx | 2 +- .../chat-sync/add-connection-modal.tsx | 2 +- .../components/chat/channel-join-banner.tsx | 2 +- .../components/chat/inline-thread-preview.tsx | 2 +- apps/web/src/components/chat/message-list.tsx | 2 +- .../components/chat/message-reply-section.tsx | 2 +- apps/web/src/components/chat/message.tsx | 2 +- .../src/components/chat/reaction-button.tsx | 2 +- .../autocomplete/triggers/emoji-trigger.tsx | 2 +- .../chat/slate-editor/mention-element.tsx | 2 +- .../slate-editor/slate-message-editor.tsx | 2 +- .../slate-editor/slate-message-viewer.tsx | 2 +- .../components/chat/thread-message-list.tsx | 2 +- apps/web/src/components/chat/thread-panel.tsx | 2 +- .../components/chat/user-profile-popover.tsx | 2 +- .../pages/create-channel-view.tsx | 2 +- .../command-palette/pages/home-view.tsx | 2 +- .../pages/join-channel-view.tsx | 2 +- .../command-palette/search-view.tsx | 2 +- .../connect/share-channel-modal.tsx | 2 +- .../src/components/embeds/use-embed-theme.ts | 2 +- .../emoji-picker/custom-emoji-section.tsx | 2 +- .../src/components/gif-picker/use-klipy.ts | 2 +- .../add-github-subscription-modal.tsx | 2 +- .../add-rss-subscription-modal.tsx | 2 +- .../edit-github-subscription-modal.tsx | 2 +- .../integrations/github-pr-embed.tsx | 2 +- .../github-subscriptions-section.tsx | 2 +- .../integrations/linear-issue-embed.tsx | 2 +- .../openstatus-integration-content.tsx | 2 +- .../railway-integration-content.tsx | 2 +- .../rss-subscriptions-section.tsx | 2 +- apps/web/src/components/link-preview.tsx | 2 +- .../components/modals/create-bot-modal.tsx | 2 +- .../modals/create-channel-modal.tsx | 2 +- .../src/components/modals/create-dm-modal.tsx | 2 +- .../modals/create-organization-modal.tsx | 2 +- .../modals/create-section-modal.tsx | 2 +- .../modals/delete-channel-modal.tsx | 2 +- .../modals/delete-workspace-modal.tsx | 2 +- .../src/components/modals/edit-bot-modal.tsx | 2 +- .../components/modals/email-invite-modal.tsx | 2 +- .../components/modals/join-channel-modal.tsx | 2 +- .../modals/rename-channel-modal.tsx | 2 +- .../components/modals/rename-thread-modal.tsx | 2 +- .../modals/request-integration-modal.tsx | 2 +- .../onboarding/invite-team-step.tsx | 2 +- .../components/onboarding/org-setup-step.tsx | 2 +- .../onboarding/profile-info-step.tsx | 2 +- .../onboarding/timezone-selection-step.tsx | 2 +- .../profile/profile-picture-upload.tsx | 2 +- .../src/components/sidebar/channel-item.tsx | 2 +- .../sidebar/discoverable-channels.tsx | 2 +- .../components/sidebar/dm-channel-item.tsx | 2 +- .../src/components/sidebar/section-group.tsx | 2 +- .../src/components/sidebar/thread-item.tsx | 2 +- .../src/components/tauri-menu-listener.tsx | 2 +- .../web/src/components/tauri-update-check.tsx | 2 +- apps/web/src/components/theme-provider.tsx | 2 +- apps/web/src/components/tweet-embed.tsx | 2 +- apps/web/src/hooks/use-bot-avatar-upload.tsx | 2 +- .../src/hooks/use-channel-member-actions.ts | 2 +- apps/web/src/hooks/use-command-palette.ts | 2 +- apps/web/src/hooks/use-emoji-stats.tsx | 2 +- apps/web/src/hooks/use-file-upload.tsx | 2 +- apps/web/src/hooks/use-loading-state.ts | 2 +- .../src/hooks/use-notification-settings.ts | 2 +- apps/web/src/hooks/use-notification-sound.tsx | 2 +- apps/web/src/hooks/use-onboarding.ts | 2 +- .../hooks/use-organization-avatar-upload.tsx | 2 +- apps/web/src/hooks/use-presence.ts | 2 +- .../src/hooks/use-profile-picture-upload.tsx | 2 +- apps/web/src/hooks/use-theme-settings.ts | 2 +- apps/web/src/hooks/use-typing.ts | 2 +- apps/web/src/hooks/use-upload.ts | 2 +- ...se-visible-message-notification-cleaner.ts | 2 +- apps/web/src/lib/auth-fetch.ts | 2 +- apps/web/src/lib/auth-token.ts | 12 +- apps/web/src/lib/auth.tsx | 2 +- apps/web/src/lib/electric-fetch.ts | 2 +- .../lib/platform-storage/platform-runtime.ts | 2 +- apps/web/src/lib/registry.ts | 2 +- apps/web/src/lib/rpc-auth-middleware.ts | 4 +- .../web/src/lib/services/common/api-client.ts | 7 +- .../src/lib/services/common/atom-client.ts | 2 +- .../services/common/link-preview-client.ts | 4 +- .../src/lib/services/common/network-mode.ts | 5 +- .../lib/services/common/rpc-atom-client.ts | 10 +- apps/web/src/lib/services/common/runtime.ts | 2 +- .../src/lib/services/desktop/tauri-auth.ts | 7 +- .../lib/services/desktop/token-exchange.ts | 9 +- .../src/lib/services/desktop/token-storage.ts | 7 +- .../web/src/lib/services/web/token-storage.ts | 7 +- apps/web/src/main.tsx | 2 +- apps/web/src/providers/chat-provider.tsx | 2 +- .../providers/notification-sound-provider.tsx | 2 +- .../channels/$channelId/settings/connect.tsx | 2 +- .../$channelId/settings/integrations.tsx | 2 +- .../channels/$channelId/settings/overview.tsx | 2 +- .../web/src/routes/_app/$orgSlug/chat/$id.tsx | 2 +- apps/web/src/routes/_app/$orgSlug/index.tsx | 2 +- .../_app/$orgSlug/my-settings/desktop.tsx | 2 +- .../$orgSlug/my-settings/linked-accounts.tsx | 2 +- .../_app/$orgSlug/my-settings/profile.tsx | 2 +- .../routes/_app/$orgSlug/profile/$userId.tsx | 2 +- .../_app/$orgSlug/settings/authentication.tsx | 2 +- .../settings/chat-sync/$connectionId.tsx | 2 +- .../$orgSlug/settings/chat-sync/index.tsx | 2 +- .../$orgSlug/settings/connect-invites.tsx | 2 +- .../_app/$orgSlug/settings/custom-emojis.tsx | 2 +- .../routes/_app/$orgSlug/settings/debug.tsx | 2 +- .../routes/_app/$orgSlug/settings/index.tsx | 2 +- .../settings/integrations/$integrationId.tsx | 2 +- .../$orgSlug/settings/integrations/index.tsx | 2 +- .../settings/integrations/installed.tsx | 2 +- .../settings/integrations/marketplace.tsx | 2 +- .../_app/$orgSlug/settings/invitations.tsx | 2 +- .../routes/_app/$orgSlug/settings/team.tsx | 2 +- .../_app/onboarding/setup-organization.tsx | 2 +- apps/web/src/routes/auth/callback.tsx | 2 +- apps/web/src/routes/auth/desktop-callback.tsx | 2 +- apps/web/src/routes/auth/desktop-login.tsx | 2 +- apps/web/src/routes/join/$slug.tsx | 2 +- bots/hazel-bot/package.json | 2 - bots/hazel-bot/src/agent-loop.ts | 2 +- bots/hazel-bot/src/degeneration-detector.ts | 2 +- bots/hazel-bot/src/errors.ts | 8 +- bots/hazel-bot/src/handler.ts | 2 +- bots/hazel-bot/src/openrouter.ts | 2 +- bots/hazel-bot/src/stream.ts | 2 +- bots/hazel-bot/src/tools/base.ts | 2 +- bots/hazel-bot/src/tools/craft.ts | 2 +- bots/hazel-bot/src/tools/linear.ts | 2 +- bots/hazel-bot/src/tools/toolkit.ts | 2 +- bots/linear-bot/package.json | 2 - bots/linear-bot/src/index.ts | 4 +- bun.lock | 332 +++++++----------- libs/ai-openrouter/package.json | 5 +- .../src/OpenRouterLanguageModel.ts | 2 +- .../bot-sdk/examples/simple-echo-bot/index.ts | 8 +- libs/bot-sdk/package.json | 15 +- libs/bot-sdk/src/auth.ts | 10 +- libs/bot-sdk/src/errors.ts | 30 +- libs/bot-sdk/src/hazel-bot-sdk.ts | 16 +- libs/bot-sdk/src/log-context.ts | 2 +- libs/bot-sdk/src/rpc/auth-middleware.ts | 4 +- libs/bot-sdk/src/rpc/client.ts | 16 +- libs/bot-sdk/src/services/health-server.ts | 12 +- libs/bot-sdk/src/streaming/actors-client.ts | 7 +- libs/bot-sdk/src/streaming/errors.ts | 12 +- .../src/streaming/streaming-service.ts | 4 +- .../package.json | 2 - .../src/collection.ts | 2 +- .../src/errors.ts | 20 +- .../src/handlers.ts | 6 +- .../src/optimistic-action.ts | 2 +- .../src/tanstack-errors.ts | 14 +- libs/tanstack-db-atom/package.json | 3 +- .../src/AtomTanStackDB.result.test.ts | 2 +- .../src/AtomTanStackDB.subscription.test.ts | 2 +- .../src/AtomTanStackDB.test.ts | 2 +- .../src/AtomTanStackDB.timing.test.ts | 2 +- libs/tanstack-db-atom/src/AtomTanStackDB.ts | 2 +- libs/tanstack-db-atom/src/types.ts | 2 +- package.json | 30 +- packages/actors/package.json | 1 - packages/actors/src/auth/config-service.ts | 5 +- packages/actors/src/auth/errors.ts | 8 +- packages/actors/src/auth/jwks-service.ts | 7 +- .../src/auth/token-validation-service.ts | 9 +- packages/actors/src/effect/runtime.ts | 2 +- packages/auth/package.json | 3 - packages/auth/src/cache/user-lookup-cache.ts | 9 +- packages/auth/src/config.ts | 7 +- packages/auth/src/consumers/backend-auth.ts | 9 +- packages/auth/src/consumers/proxy-auth.ts | 15 +- packages/auth/src/errors.ts | 6 +- packages/auth/src/session/workos-client.ts | 7 +- packages/backend-core/package.json | 2 - .../src/repositories/attachment-repo.ts | 7 +- .../src/repositories/bot-command-repo.ts | 7 +- .../src/repositories/bot-installation-repo.ts | 7 +- .../backend-core/src/repositories/bot-repo.ts | 7 +- .../src/repositories/channel-member-repo.ts | 7 +- .../src/repositories/channel-repo.ts | 7 +- .../src/repositories/channel-section-repo.ts | 7 +- .../src/repositories/channel-webhook-repo.ts | 7 +- .../chat-sync-channel-link-repo.ts | 5 +- .../repositories/chat-sync-connection-repo.ts | 5 +- .../chat-sync-event-receipt-repo.ts | 5 +- .../chat-sync-message-link-repo.ts | 5 +- .../connect-conversation-channel-repo.ts | 5 +- .../repositories/connect-conversation-repo.ts | 5 +- .../src/repositories/connect-invite-repo.ts | 7 +- .../repositories/connect-participant-repo.ts | 5 +- .../src/repositories/custom-emoji-repo.ts | 7 +- .../repositories/github-subscription-repo.ts | 5 +- .../integration-connection-repo.ts | 5 +- .../repositories/integration-token-repo.ts | 7 +- .../src/repositories/invitation-repo.ts | 7 +- .../src/repositories/message-outbox-repo.ts | 7 +- .../src/repositories/message-reaction-repo.ts | 7 +- .../src/repositories/message-repo.ts | 7 +- .../src/repositories/notification-repo.ts | 7 +- .../repositories/organization-member-repo.ts | 5 +- .../src/repositories/organization-repo.ts | 7 +- .../src/repositories/pinned-message-repo.ts | 7 +- .../src/repositories/rss-subscription-repo.ts | 7 +- .../src/repositories/typing-indicator-repo.ts | 7 +- .../repositories/user-presence-status-repo.ts | 5 +- .../src/repositories/user-repo.ts | 7 +- .../backend-core/src/services/workos-sync.ts | 11 +- packages/backend-core/src/services/workos.ts | 9 +- packages/db/package.json | 2 - packages/db/src/services/database.ts | 4 +- packages/db/src/services/model.ts | 2 +- packages/domain/package.json | 6 - .../src/cluster/activities/bot-activities.ts | 2 +- .../cluster/activities/cleanup-activities.ts | 4 +- .../cluster/activities/github-activities.ts | 4 +- .../github-installation-activities.ts | 4 +- .../cluster/activities/message-activities.ts | 4 +- .../src/cluster/activities/rss-activities.ts | 6 +- .../activities/thread-naming-activities.ts | 14 +- packages/domain/src/cluster/api.ts | 4 +- .../workflows/cleanup-uploads-workflow.ts | 2 +- .../workflows/github-installation-workflow.ts | 2 +- .../workflows/github-webhook-workflow.ts | 2 +- .../message-notification-workflow.ts | 2 +- .../workflows/rss-feed-poll-workflow.ts | 2 +- .../workflows/thread-naming-workflow.ts | 2 +- packages/domain/src/current-user.ts | 2 +- packages/domain/src/desktop-auth-errors.ts | 22 +- packages/domain/src/errors.ts | 50 +-- packages/domain/src/http/api-v1/messages.ts | 10 +- packages/domain/src/http/api.ts | 2 +- packages/domain/src/http/auth.ts | 2 +- packages/domain/src/http/bot-commands.ts | 12 +- packages/domain/src/http/chat-sync.ts | 12 +- packages/domain/src/http/incoming-webhooks.ts | 14 +- .../domain/src/http/integration-commands.ts | 2 +- .../domain/src/http/integration-resources.ts | 8 +- packages/domain/src/http/integrations.ts | 10 +- packages/domain/src/http/internal.ts | 2 +- packages/domain/src/http/klipy.ts | 8 +- packages/domain/src/http/mock-data.ts | 2 +- packages/domain/src/http/presence.ts | 2 +- packages/domain/src/http/root.ts | 2 +- packages/domain/src/http/uploads.ts | 20 +- packages/domain/src/http/webhooks.ts | 14 +- packages/domain/src/rate-limit-errors.ts | 8 +- packages/domain/src/rpc/attachments.ts | 4 +- packages/domain/src/rpc/bots.ts | 6 +- packages/domain/src/rpc/channel-members.ts | 4 +- packages/domain/src/rpc/channel-sections.ts | 4 +- packages/domain/src/rpc/channel-webhooks.ts | 4 +- packages/domain/src/rpc/channels.ts | 4 +- packages/domain/src/rpc/chat-sync.ts | 12 +- packages/domain/src/rpc/connect-shares.ts | 10 +- packages/domain/src/rpc/custom-emojis.ts | 8 +- .../domain/src/rpc/github-subscriptions.ts | 8 +- .../domain/src/rpc/integration-requests.ts | 2 +- packages/domain/src/rpc/invitations.ts | 4 +- packages/domain/src/rpc/message-reactions.ts | 4 +- packages/domain/src/rpc/messages.ts | 2 +- packages/domain/src/rpc/middleware.ts | 2 +- packages/domain/src/rpc/notifications.ts | 4 +- .../domain/src/rpc/organization-members.ts | 4 +- packages/domain/src/rpc/organizations.ts | 10 +- packages/domain/src/rpc/pinned-messages.ts | 4 +- packages/domain/src/rpc/rss-subscriptions.ts | 8 +- .../src/rpc/scope-injection-middleware.ts | 2 +- packages/domain/src/rpc/typing-indicators.ts | 4 +- .../domain/src/rpc/user-presence-status.ts | 4 +- packages/domain/src/rpc/users.ts | 4 +- .../domain/src/scopes/permission-error.ts | 8 +- packages/domain/src/scopes/required-scopes.ts | 7 +- packages/domain/src/session-errors.ts | 50 +-- packages/effect-bun/package.json | 3 - packages/effect-bun/src/Redis.ts | 8 +- packages/effect-bun/src/S3.ts | 14 +- .../src/persistence/redis-backing.ts | 2 +- packages/integrations/package.json | 2 - packages/integrations/src/craft/api-client.ts | 25 +- .../integrations/src/discord/api-client.ts | 11 +- .../integrations/src/github/api-client.ts | 27 +- .../integrations/src/github/jwt-service.ts | 15 +- .../integrations/src/linear/api-client.ts | 17 +- packages/rivet-effect/package.json | 3 +- packages/rivet-effect/src/actor.ts | 15 +- packages/rivet-effect/src/errors.ts | 4 +- packages/rivet-effect/src/lifecycle.ts | 2 +- packages/rivet-effect/src/runtime.ts | 2 +- packages/schema/package.json | 2 - packages/schema/src/avatar-url.ts | 6 +- packages/setup/package.json | 5 - packages/setup/src/commands/bots.ts | 2 +- packages/setup/src/commands/certs.ts | 2 +- packages/setup/src/commands/doctor.ts | 2 +- packages/setup/src/commands/env.ts | 2 +- packages/setup/src/commands/setup.ts | 6 +- packages/setup/src/index.ts | 2 +- packages/setup/src/prompts.ts | 2 +- packages/setup/src/services/cert-manager.ts | 9 +- packages/setup/src/services/doctor.ts | 15 +- packages/setup/src/services/env-writer.ts | 7 +- packages/setup/src/services/secrets.ts | 7 +- packages/setup/src/services/validators.ts | 7 +- 458 files changed, 1156 insertions(+), 1479 deletions(-) diff --git a/.context/README.md b/.context/README.md index 1d1151aad..557c930fb 100644 --- a/.context/README.md +++ b/.context/README.md @@ -7,9 +7,9 @@ This directory contains git subtrees of library documentation and examples for r The following repositories are included as git subtrees: - **Effect** (`.context/effect/`) - - Repository: https://github.com/Effect-TS/effect + - Repository: https://github.com/Effect-TS/effect-smol - Branch: main - - Purpose: Effect-TS functional programming library documentation and examples + - Purpose: Effect v4 (effect-smol) functional programming library documentation and examples - **Effect Atom** (`.context/effect-atom/`) - Repository: https://github.com/tim-smart/effect-atom @@ -39,7 +39,7 @@ git subtree pull --prefix=.context/tanstack-db tanstack-db-subtree main --squash Note: The git remotes should already be configured. If not, add them first: ```bash -git remote add effect-subtree https://github.com/Effect-TS/effect +git remote add effect-subtree https://github.com/Effect-TS/effect-smol git remote add effect-atom-subtree https://github.com/tim-smart/effect-atom git remote add tanstack-db-subtree https://github.com/TanStack/db ``` diff --git a/apps/backend/package.json b/apps/backend/package.json index a0f97d41f..8620121b1 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -24,14 +24,9 @@ "rebuild-channel-access": "bun run scripts/rebuild-channel-access.ts" }, "dependencies": { - "@effect/cluster": "catalog:effect", - "@effect/experimental": "catalog:effect", "@effect/opentelemetry": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-bun": "catalog:effect", "@effect/platform-node": "catalog:effect", - "@effect/rpc": "catalog:effect", - "@effect/sql": "catalog:effect", "@effect/sql-pg": "catalog:effect", "@hazel/auth": "workspace:*", "@hazel/backend-core": "workspace:*", @@ -48,7 +43,6 @@ "pg": "^8.16.3" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@testcontainers/postgresql": "^10.18.0", "@types/bun": "1.3.9", "drizzle-kit": "^0.31.8", diff --git a/apps/backend/scripts/test-mock-data.ts b/apps/backend/scripts/test-mock-data.ts index 5f84b4a25..7c7c4655d 100644 --- a/apps/backend/scripts/test-mock-data.ts +++ b/apps/backend/scripts/test-mock-data.ts @@ -168,7 +168,7 @@ const runTests = Effect.gen(function* () { yield* Console.error("\n⚠️ Some tests failed") } }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Console.error("\n❌ Test suite error:", error) yield* Console.error("\n💡 Make sure the backend is running on port 3003") diff --git a/apps/backend/src/http.ts b/apps/backend/src/http.ts index ef17685ca..b1a248329 100644 --- a/apps/backend/src/http.ts +++ b/apps/backend/src/http.ts @@ -1,4 +1,4 @@ -import { HttpLayerRouter } from "@effect/platform" +import { HttpRouter } from "effect/unstable/http" import { Layer } from "effect" import { HazelApi } from "./api" import { HttpMessagesApiLive } from "./routes/api-v1" @@ -17,7 +17,7 @@ import { HttpRootLive } from "./routes/root.http" import { HttpUploadsLive } from "./routes/uploads.http" import { HttpWebhookLive } from "./routes/webhooks.http" -export const HttpApiRoutes = HttpLayerRouter.addHttpApi(HazelApi).pipe( +export const HttpApiRoutes = HttpRouter.addHttpApi(HazelApi).pipe( Layer.provide(HttpRootLive), Layer.provide(HttpAuthLive), Layer.provide(HttpMessagesApiLive), diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index c1443e386..ce4534b21 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,12 +1,7 @@ -import { - FetchHttpClient, - HttpApiScalar, - HttpLayerRouter, - HttpMiddleware, - HttpServerResponse, -} from "@effect/platform" +import { HttpApiScalar } from "effect/unstable/httpapi" +import { FetchHttpClient, HttpRouter, HttpMiddleware, HttpServerResponse } from "effect/unstable/http" import { BunHttpServer, BunRuntime } from "@effect/platform-bun" -import { RpcSerialization, RpcServer } from "@effect/rpc" +import { RpcSerialization, RpcServer } from "effect/unstable/rpc" import { AttachmentRepo, BotCommandRepo, @@ -96,11 +91,11 @@ export { HazelApi } // Export RPC groups for frontend consumption export { AuthMiddleware, InvitationRpcs, MessageRpcs, NotificationRpcs } from "@hazel/domain/rpc" -const HealthRouter = HttpLayerRouter.use((router) => +const HealthRouter = HttpRouter.use((router) => router.add("GET", "/health", HttpServerResponse.text("OK")), ) -const DocsRoute = HttpApiScalar.layerHttpLayerRouter({ +const DocsRoute = HttpApiScalar.layerHttpRouter({ api: HazelApi, path: "/docs", }) @@ -114,7 +109,7 @@ const RpcRoute = RpcServer.layerHttpRouter({ const AllRoutes = Layer.mergeAll(HttpApiRoutes, HealthRouter, DocsRoute, RpcRoute).pipe( Layer.provide( - HttpLayerRouter.cors({ + HttpRouter.cors({ allowedOrigins: [ "http://localhost:3000", "http://localhost:5173", @@ -226,7 +221,7 @@ const MainLive = Layer.mergeAll( Layer.provideMerge(Layer.setConfigProvider(ConfigProvider.fromEnv())), ) -const ServerLayer = HttpLayerRouter.serve(AllRoutes).pipe( +const ServerLayer = HttpRouter.serve(AllRoutes).pipe( HttpMiddleware.withTracerDisabledWhen( (request) => request.url === "/health" || request.method === "OPTIONS", ), diff --git a/apps/backend/src/lib/env-vars.ts b/apps/backend/src/lib/env-vars.ts index a6f2444eb..82527a426 100644 --- a/apps/backend/src/lib/env-vars.ts +++ b/apps/backend/src/lib/env-vars.ts @@ -1,9 +1,8 @@ import * as Config from "effect/Config" import * as Effect from "effect/Effect" -export class EnvVars extends Effect.Service()("EnvVars", { - accessors: true, - effect: Effect.gen(function* () { +export class EnvVars extends ServiceMap.Service()("EnvVars", { + make: Effect.gen(function* () { return { IS_DEV: yield* Config.boolean("IS_DEV").pipe(Config.withDefault(false)), DATABASE_URL: yield* Config.redacted("DATABASE_URL"), diff --git a/apps/backend/src/lib/policy-utils.ts b/apps/backend/src/lib/policy-utils.ts index 3022cb15c..d23e21531 100644 --- a/apps/backend/src/lib/policy-utils.ts +++ b/apps/backend/src/lib/policy-utils.ts @@ -27,7 +27,7 @@ export const makePolicy = export const withPolicyUnauthorized = ( entity: string, action: string, - effect: Effect.Effect, + make: Effect.Effect, ) => ErrorUtils.refailUnauthorized(entity, action)(effect) /** diff --git a/apps/backend/src/policies/attachment-policy.ts b/apps/backend/src/policies/attachment-policy.ts index abd0555d0..eff4cd292 100644 --- a/apps/backend/src/policies/attachment-policy.ts +++ b/apps/backend/src/policies/attachment-policy.ts @@ -7,12 +7,12 @@ import { } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { AttachmentId } from "@hazel/schema" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" import { isAdminOrOwner } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" -export class AttachmentPolicy extends Effect.Service()("AttachmentPolicy/Policy", { - effect: Effect.gen(function* () { +export class AttachmentPolicy extends ServiceMap.Service()("AttachmentPolicy/Policy", { + make: Effect.gen(function* () { const policyEntity = "Attachment" as const const attachmentRepo = yield* AttachmentRepo @@ -168,5 +168,4 @@ export class AttachmentPolicy extends Effect.Service()("Attach ChannelMemberRepo.Default, OrgResolver.Default, ], - accessors: true, }) {} diff --git a/apps/backend/src/policies/bot-policy.ts b/apps/backend/src/policies/bot-policy.ts index 380944804..d923bd6cd 100644 --- a/apps/backend/src/policies/bot-policy.ts +++ b/apps/backend/src/policies/bot-policy.ts @@ -1,13 +1,13 @@ import { BotRepo } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { BotId, OrganizationId } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" /** @effect-leakable-service */ -export class BotPolicy extends Effect.Service()("BotPolicy/Policy", { - effect: Effect.gen(function* () { +export class BotPolicy extends ServiceMap.Service()("BotPolicy/Policy", { + make: Effect.gen(function* () { const policyEntity = "Bot" as const const botRepo = yield* BotRepo @@ -110,5 +110,4 @@ export class BotPolicy extends Effect.Service()("BotPolicy/Policy", { return { canCreate, canRead, canUpdate, canDelete, canInstall, canUninstall } as const }), dependencies: [BotRepo.Default, OrgResolver.Default], - accessors: true, }) {} diff --git a/apps/backend/src/policies/channel-member-policy.ts b/apps/backend/src/policies/channel-member-policy.ts index 6774f5cc0..5c921fa64 100644 --- a/apps/backend/src/policies/channel-member-policy.ts +++ b/apps/backend/src/policies/channel-member-policy.ts @@ -1,12 +1,12 @@ import { ChannelMemberRepo, ChannelRepo, OrganizationMemberRepo } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { ChannelId, ChannelMemberId } from "@hazel/schema" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" import { isAdminOrOwner } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" -export class ChannelMemberPolicy extends Effect.Service()("ChannelMemberPolicy/Policy", { - effect: Effect.gen(function* () { +export class ChannelMemberPolicy extends ServiceMap.Service()("ChannelMemberPolicy/Policy", { + make: Effect.gen(function* () { const policyEntity = "ChannelMember" as const const channelMemberRepo = yield* ChannelMemberRepo @@ -170,5 +170,4 @@ export class ChannelMemberPolicy extends Effect.Service()(" OrganizationMemberRepo.Default, OrgResolver.Default, ], - accessors: true, }) {} diff --git a/apps/backend/src/policies/channel-policy.ts b/apps/backend/src/policies/channel-policy.ts index ac07f1d22..5024ac1e6 100644 --- a/apps/backend/src/policies/channel-policy.ts +++ b/apps/backend/src/policies/channel-policy.ts @@ -1,12 +1,12 @@ import { ChannelRepo } from "@hazel/backend-core" import { ErrorUtils } from "@hazel/domain" import type { ChannelId, OrganizationId } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" -export class ChannelPolicy extends Effect.Service()("ChannelPolicy/Policy", { - effect: Effect.gen(function* () { +export class ChannelPolicy extends ServiceMap.Service()("ChannelPolicy/Policy", { + make: Effect.gen(function* () { const policyEntity = "Channel" as const const orgResolver = yield* OrgResolver @@ -59,5 +59,4 @@ export class ChannelPolicy extends Effect.Service()("ChannelPolic return { canUpdate, canDelete, canCreate } as const }), dependencies: [ChannelRepo.Default, OrgResolver.Default], - accessors: true, }) {} diff --git a/apps/backend/src/policies/channel-section-policy.ts b/apps/backend/src/policies/channel-section-policy.ts index adaa42e3b..fec9b0b9b 100644 --- a/apps/backend/src/policies/channel-section-policy.ts +++ b/apps/backend/src/policies/channel-section-policy.ts @@ -1,11 +1,11 @@ import { ChannelSectionRepo } from "@hazel/backend-core" import { ErrorUtils } from "@hazel/domain" import type { ChannelSectionId, OrganizationId } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" -export class ChannelSectionPolicy extends Effect.Service()( +export class ChannelSectionPolicy extends ServiceMap.Service()( "ChannelSectionPolicy/Policy", { effect: Effect.gen(function* () { @@ -71,6 +71,5 @@ export class ChannelSectionPolicy extends Effect.Service() return { canCreate, canUpdate, canDelete, canReorder } as const }), dependencies: [ChannelSectionRepo.Default, OrgResolver.Default], - accessors: true, }, ) {} diff --git a/apps/backend/src/policies/channel-webhook-policy.ts b/apps/backend/src/policies/channel-webhook-policy.ts index 73c460783..d03c32db2 100644 --- a/apps/backend/src/policies/channel-webhook-policy.ts +++ b/apps/backend/src/policies/channel-webhook-policy.ts @@ -1,12 +1,12 @@ import { ChannelRepo, ChannelWebhookRepo } from "@hazel/backend-core" import { ErrorUtils } from "@hazel/domain" import type { ChannelId, ChannelWebhookId } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" /** @effect-leakable-service */ -export class ChannelWebhookPolicy extends Effect.Service()( +export class ChannelWebhookPolicy extends ServiceMap.Service()( "ChannelWebhookPolicy/Policy", { effect: Effect.gen(function* () { @@ -87,6 +87,5 @@ export class ChannelWebhookPolicy extends Effect.Service() return { canCreate, canRead, canUpdate, canDelete } as const }), dependencies: [ChannelRepo.Default, ChannelWebhookRepo.Default, OrgResolver.Default], - accessors: true, }, ) {} diff --git a/apps/backend/src/policies/custom-emoji-policy.ts b/apps/backend/src/policies/custom-emoji-policy.ts index ddff15f16..6b57ffa93 100644 --- a/apps/backend/src/policies/custom-emoji-policy.ts +++ b/apps/backend/src/policies/custom-emoji-policy.ts @@ -1,12 +1,12 @@ import { CustomEmojiRepo } from "@hazel/backend-core" import { ErrorUtils } from "@hazel/domain" import type { CustomEmojiId, OrganizationId } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" -export class CustomEmojiPolicy extends Effect.Service()("CustomEmojiPolicy/Policy", { - effect: Effect.gen(function* () { +export class CustomEmojiPolicy extends ServiceMap.Service()("CustomEmojiPolicy/Policy", { + make: Effect.gen(function* () { const policyEntity = "CustomEmoji" as const const orgResolver = yield* OrgResolver @@ -49,5 +49,4 @@ export class CustomEmojiPolicy extends Effect.Service()("Cust return { canCreate, canUpdate, canDelete } as const }), dependencies: [CustomEmojiRepo.Default, OrgResolver.Default], - accessors: true, }) {} diff --git a/apps/backend/src/policies/github-subscription-policy.ts b/apps/backend/src/policies/github-subscription-policy.ts index 50f37f513..3ebaf920b 100644 --- a/apps/backend/src/policies/github-subscription-policy.ts +++ b/apps/backend/src/policies/github-subscription-policy.ts @@ -1,12 +1,12 @@ import { ChannelRepo, GitHubSubscriptionRepo } from "@hazel/backend-core" import { ErrorUtils } from "@hazel/domain" import type { ChannelId, GitHubSubscriptionId, OrganizationId } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" /** @effect-leakable-service */ -export class GitHubSubscriptionPolicy extends Effect.Service()( +export class GitHubSubscriptionPolicy extends ServiceMap.Service()( "GitHubSubscriptionPolicy/Policy", { effect: Effect.gen(function* () { @@ -97,6 +97,5 @@ export class GitHubSubscriptionPolicy extends Effect.Service()( +export class IntegrationConnectionPolicy extends ServiceMap.Service()( "IntegrationConnectionPolicy/Policy", { effect: Effect.gen(function* () { @@ -55,6 +55,5 @@ export class IntegrationConnectionPolicy extends Effect.Service()("InvitationPolicy/Policy", { - effect: Effect.gen(function* () { +export class InvitationPolicy extends ServiceMap.Service()("InvitationPolicy/Policy", { + make: Effect.gen(function* () { const policyEntity = "Invitation" as const const invitationRepo = yield* InvitationRepo @@ -147,5 +147,4 @@ export class InvitationPolicy extends Effect.Service()("Invita UserRepo.Default, OrgResolver.Default, ], - accessors: true, }) {} diff --git a/apps/backend/src/policies/message-policy.ts b/apps/backend/src/policies/message-policy.ts index 98b649c08..0db6e3a3d 100644 --- a/apps/backend/src/policies/message-policy.ts +++ b/apps/backend/src/policies/message-policy.ts @@ -1,12 +1,12 @@ import { ChannelRepo, MessageRepo, OrganizationMemberRepo } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { ChannelId, MessageId } from "@hazel/schema" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" import { isAdminOrOwner, withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" -export class MessagePolicy extends Effect.Service()("MessagePolicy/Policy", { - effect: Effect.gen(function* () { +export class MessagePolicy extends ServiceMap.Service()("MessagePolicy/Policy", { + make: Effect.gen(function* () { const policyEntity = "Message" as const const messageRepo = yield* MessageRepo @@ -92,5 +92,4 @@ export class MessagePolicy extends Effect.Service()("MessagePolic OrganizationMemberRepo.Default, OrgResolver.Default, ], - accessors: true, }) {} diff --git a/apps/backend/src/policies/message-reaction-policy.ts b/apps/backend/src/policies/message-reaction-policy.ts index e8237278a..12b228f51 100644 --- a/apps/backend/src/policies/message-reaction-policy.ts +++ b/apps/backend/src/policies/message-reaction-policy.ts @@ -1,12 +1,12 @@ import { MessageReactionRepo, MessageRepo } from "@hazel/backend-core" import { ErrorUtils, PermissionError, policy } from "@hazel/domain" import type { MessageId, MessageReactionId } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { ConnectConversationService } from "../services/connect-conversation-service" import { OrgResolver } from "../services/org-resolver" -export class MessageReactionPolicy extends Effect.Service()( +export class MessageReactionPolicy extends ServiceMap.Service()( "MessageReactionPolicy/Policy", { effect: Effect.gen(function* () { @@ -105,6 +105,5 @@ export class MessageReactionPolicy extends Effect.Service OrgResolver.Default, ConnectConversationService.Default, ], - accessors: true, }, ) {} diff --git a/apps/backend/src/policies/notification-policy.ts b/apps/backend/src/policies/notification-policy.ts index 03788ba33..feff3bf84 100644 --- a/apps/backend/src/policies/notification-policy.ts +++ b/apps/backend/src/policies/notification-policy.ts @@ -1,11 +1,11 @@ import { NotificationRepo, OrganizationMemberRepo } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { NotificationId, OrganizationMemberId } from "@hazel/schema" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" import { isAdminOrOwner } from "../lib/policy-utils" -export class NotificationPolicy extends Effect.Service()("NotificationPolicy/Policy", { - effect: Effect.gen(function* () { +export class NotificationPolicy extends ServiceMap.Service()("NotificationPolicy/Policy", { + make: Effect.gen(function* () { const policyEntity = "Notification" as const const notificationRepo = yield* NotificationRepo @@ -174,5 +174,4 @@ export class NotificationPolicy extends Effect.Service()("No return { canCreate, canView, canUpdate, canDelete, canMarkAsRead, canMarkAllAsRead } as const }), dependencies: [NotificationRepo.Default, OrganizationMemberRepo.Default], - accessors: true, }) {} diff --git a/apps/backend/src/policies/organization-member-policy.ts b/apps/backend/src/policies/organization-member-policy.ts index d13d3091b..de7013b1f 100644 --- a/apps/backend/src/policies/organization-member-policy.ts +++ b/apps/backend/src/policies/organization-member-policy.ts @@ -1,10 +1,10 @@ import { OrganizationMemberRepo } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { OrganizationId, OrganizationMemberId } from "@hazel/schema" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" import { OrgResolver } from "../services/org-resolver" -export class OrganizationMemberPolicy extends Effect.Service()( +export class OrganizationMemberPolicy extends ServiceMap.Service()( "OrganizationMemberPolicy/Policy", { effect: Effect.gen(function* () { @@ -104,6 +104,5 @@ export class OrganizationMemberPolicy extends Effect.Service()("OrganizationPolicy/Policy", { - effect: Effect.gen(function* () { +export class OrganizationPolicy extends ServiceMap.Service()("OrganizationPolicy/Policy", { + make: Effect.gen(function* () { const policyEntity = "Organization" as const const authorize = makePolicy(policyEntity) @@ -48,5 +48,4 @@ export class OrganizationPolicy extends Effect.Service()("Or return { canUpdate, canDelete, canCreate, isMember, canManagePublicInvite } as const }), dependencies: [OrgResolver.Default], - accessors: true, }) {} diff --git a/apps/backend/src/policies/pinned-message-policy.ts b/apps/backend/src/policies/pinned-message-policy.ts index 3cf600e3c..5585cf485 100644 --- a/apps/backend/src/policies/pinned-message-policy.ts +++ b/apps/backend/src/policies/pinned-message-policy.ts @@ -1,12 +1,12 @@ import { ChannelRepo, OrganizationMemberRepo, PinnedMessageRepo } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { ChannelId, PinnedMessageId } from "@hazel/schema" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" import { isAdminOrOwner } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" -export class PinnedMessagePolicy extends Effect.Service()("PinnedMessagePolicy/Policy", { - effect: Effect.gen(function* () { +export class PinnedMessagePolicy extends ServiceMap.Service()("PinnedMessagePolicy/Policy", { + make: Effect.gen(function* () { const policyEntity = "PinnedMessage" as const const pinnedMessageRepo = yield* PinnedMessageRepo @@ -118,5 +118,4 @@ export class PinnedMessagePolicy extends Effect.Service()(" OrganizationMemberRepo.Default, OrgResolver.Default, ], - accessors: true, }) {} diff --git a/apps/backend/src/policies/policy-test-helpers.ts b/apps/backend/src/policies/policy-test-helpers.ts index 37b6092f9..ce61dd964 100644 --- a/apps/backend/src/policies/policy-test-helpers.ts +++ b/apps/backend/src/policies/policy-test-helpers.ts @@ -25,7 +25,7 @@ export const makeActor = (overrides?: Partial): CurrentUser. }) export const runWithActorEither = ( - effect: Effect.Effect, + make: Effect.Effect, layer: Layer.Layer, actor: CurrentUser.Schema = makeActor(), scopes: ReadonlyArray = ["messages:read"], diff --git a/apps/backend/src/policies/rss-subscription-policy.ts b/apps/backend/src/policies/rss-subscription-policy.ts index 35872bcb9..8e4a64b46 100644 --- a/apps/backend/src/policies/rss-subscription-policy.ts +++ b/apps/backend/src/policies/rss-subscription-policy.ts @@ -1,12 +1,12 @@ import { ChannelRepo, RssSubscriptionRepo } from "@hazel/backend-core" import { ErrorUtils } from "@hazel/domain" import type { ChannelId, OrganizationId, RssSubscriptionId } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" /** @effect-leakable-service */ -export class RssSubscriptionPolicy extends Effect.Service()( +export class RssSubscriptionPolicy extends ServiceMap.Service()( "RssSubscriptionPolicy/Policy", { effect: Effect.gen(function* () { @@ -97,6 +97,5 @@ export class RssSubscriptionPolicy extends Effect.Service return { canCreate, canRead, canReadByOrganization, canUpdate, canDelete } as const }), dependencies: [ChannelRepo.Default, RssSubscriptionRepo.Default, OrgResolver.Default], - accessors: true, }, ) {} diff --git a/apps/backend/src/policies/typing-indicator-policy.ts b/apps/backend/src/policies/typing-indicator-policy.ts index 74a030b41..ecd4b3309 100644 --- a/apps/backend/src/policies/typing-indicator-policy.ts +++ b/apps/backend/src/policies/typing-indicator-policy.ts @@ -1,9 +1,9 @@ import { ChannelMemberRepo, TypingIndicatorRepo } from "@hazel/backend-core" import type { ChannelId, ChannelMemberId, TypingIndicatorId } from "@hazel/schema" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" import { makePolicy, withPolicyUnauthorized } from "../lib/policy-utils" -export class TypingIndicatorPolicy extends Effect.Service()( +export class TypingIndicatorPolicy extends ServiceMap.Service()( "TypingIndicatorPolicy/Policy", { effect: Effect.gen(function* () { @@ -53,6 +53,5 @@ export class TypingIndicatorPolicy extends Effect.Service return { canCreate, canUpdate, canDelete, canRead } as const }), dependencies: [ChannelMemberRepo.Default, TypingIndicatorRepo.Default], - accessors: true, }, ) {} diff --git a/apps/backend/src/policies/user-policy.ts b/apps/backend/src/policies/user-policy.ts index eb2523b8e..7b92551eb 100644 --- a/apps/backend/src/policies/user-policy.ts +++ b/apps/backend/src/policies/user-policy.ts @@ -1,9 +1,9 @@ import type { UserId } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { makePolicy } from "../lib/policy-utils" -export class UserPolicy extends Effect.Service()("UserPolicy/Policy", { - effect: Effect.gen(function* () { +export class UserPolicy extends ServiceMap.Service()("UserPolicy/Policy", { + make: Effect.gen(function* () { const policyEntity = "User" as const const authorize = makePolicy(policyEntity) @@ -18,5 +18,4 @@ export class UserPolicy extends Effect.Service()("UserPolicy/Policy" return { canCreate, canUpdate, canDelete, canRead } as const }), dependencies: [], - accessors: true, }) {} diff --git a/apps/backend/src/policies/user-presence-status-policy.ts b/apps/backend/src/policies/user-presence-status-policy.ts index 9b3741fc2..c99eb722e 100644 --- a/apps/backend/src/policies/user-presence-status-policy.ts +++ b/apps/backend/src/policies/user-presence-status-policy.ts @@ -1,7 +1,7 @@ -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { makePolicy } from "../lib/policy-utils" -export class UserPresenceStatusPolicy extends Effect.Service()( +export class UserPresenceStatusPolicy extends ServiceMap.Service()( "UserPresenceStatusPolicy/Policy", { effect: Effect.gen(function* () { @@ -19,6 +19,5 @@ export class UserPresenceStatusPolicy extends Effect.Service( scopes: ReadonlyArray, - effect: Effect.Effect, + make: Effect.Effect, ): Effect.Effect => Effect.locally(CurrentRpcScopes, scopes)(effect) export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messages", (handlers) => diff --git a/apps/backend/src/routes/auth.http.ts b/apps/backend/src/routes/auth.http.ts index b81e77a5f..95fe7c281 100644 --- a/apps/backend/src/routes/auth.http.ts +++ b/apps/backend/src/routes/auth.http.ts @@ -1,4 +1,5 @@ -import { HttpApiBuilder, HttpServerResponse } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpServerResponse } from "effect/unstable/http" import { getJwtExpiry } from "@hazel/auth" import { UserRepo } from "@hazel/backend-core" import { WorkOSUserId } from "@hazel/schema" diff --git a/apps/backend/src/routes/bot-commands.http.ts b/apps/backend/src/routes/bot-commands.http.ts index 88e5c5836..0a18addbf 100644 --- a/apps/backend/src/routes/bot-commands.http.ts +++ b/apps/backend/src/routes/bot-commands.http.ts @@ -1,5 +1,6 @@ -import { HttpApiBuilder, HttpServerRequest, HttpServerResponse } from "@effect/platform" -import { Sse } from "@effect/experimental" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { Sse } from "effect/unstable/encoding" import { BotCommandRepo, BotInstallationRepo, BotRepo, IntegrationConnectionRepo } from "@hazel/backend-core" import { CurrentUser, InternalServerError, UnauthorizedError } from "@hazel/domain" import { @@ -130,7 +131,7 @@ export const createCommandSseStream = ({ Effect.tap(() => Effect.logDebug(`Bot ${botId} (${botName}) disconnected from SSE stream`), ), - Effect.catchAll(() => Effect.void), + Effect.catch(() => Effect.void), ), ) @@ -138,7 +139,7 @@ export const createCommandSseStream = ({ yield* Effect.never }).pipe( Effect.scoped, - Effect.catchAll((error) => { + Effect.catch((error) => { // Log the error but don't fail the stream - end it gracefully Effect.runFork(Effect.logError("Redis subscription error", { error, botId, botName })) emit.end() diff --git a/apps/backend/src/routes/chat-sync.http.ts b/apps/backend/src/routes/chat-sync.http.ts index 0430142b4..b544d8644 100644 --- a/apps/backend/src/routes/chat-sync.http.ts +++ b/apps/backend/src/routes/chat-sync.http.ts @@ -17,7 +17,7 @@ import { } from "@hazel/backend-core" import { ExternalChannelId } from "@hazel/schema" import { CurrentUser, InternalServerError, UnauthorizedError } from "@hazel/domain" -import { HttpApiBuilder } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { Effect, Option } from "effect" import { HazelApi } from "../api" import { generateTransactionId } from "../lib/create-transactionId" diff --git a/apps/backend/src/routes/incoming-webhooks.http.ts b/apps/backend/src/routes/incoming-webhooks.http.ts index 5fcd68e77..c5381bb8e 100644 --- a/apps/backend/src/routes/incoming-webhooks.http.ts +++ b/apps/backend/src/routes/incoming-webhooks.http.ts @@ -1,5 +1,5 @@ import { createHash, timingSafeEqual } from "node:crypto" -import { HttpApiBuilder } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { ChannelWebhookRepo, MessageOutboxRepo, MessageRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import type { MessageEmbed as DbMessageEmbed } from "@hazel/db" diff --git a/apps/backend/src/routes/integration-commands.http.ts b/apps/backend/src/routes/integration-commands.http.ts index ba910ec03..c662054d4 100644 --- a/apps/backend/src/routes/integration-commands.http.ts +++ b/apps/backend/src/routes/integration-commands.http.ts @@ -1,4 +1,4 @@ -import { HttpApiBuilder } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { BotCommandRepo, BotInstallationRepo, BotRepo } from "@hazel/backend-core" import { InternalServerError } from "@hazel/domain" import { AvailableCommandsResponse } from "@hazel/domain/http" diff --git a/apps/backend/src/routes/integration-resources.http.ts b/apps/backend/src/routes/integration-resources.http.ts index 21ee5d55f..68927801b 100644 --- a/apps/backend/src/routes/integration-resources.http.ts +++ b/apps/backend/src/routes/integration-resources.http.ts @@ -1,4 +1,4 @@ -import { HttpApiBuilder } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { IntegrationConnectionRepo } from "@hazel/backend-core" import { InternalServerError } from "@hazel/domain" import type { ExternalChannelId, OrganizationId } from "@hazel/schema" diff --git a/apps/backend/src/routes/integrations.http.ts b/apps/backend/src/routes/integrations.http.ts index a2bac4284..a71652034 100644 --- a/apps/backend/src/routes/integrations.http.ts +++ b/apps/backend/src/routes/integrations.http.ts @@ -1,5 +1,5 @@ -import { HttpApiBuilder, HttpServerRequest, HttpServerResponse } from "@effect/platform" -import * as Cookies from "@effect/platform/Cookies" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpServerRequest, HttpServerResponse, Cookies } from "effect/unstable/http" import { IntegrationConnectionRepo, OrganizationRepo } from "@hazel/backend-core" import { CurrentUser, InternalServerError, UnauthorizedError } from "@hazel/domain" import type { OrganizationId, UserId } from "@hazel/schema" @@ -923,7 +923,7 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( ), // Note: catchAll is intentional here - this is a best-effort operation // after OAuth success. We catch all errors to prevent disrupting the flow. - Effect.catchAll((error) => + Effect.catch((error) => Effect.logWarning("Failed to add integration bot to org (non-critical)", { event: "integration_bot_add_failed", provider, @@ -1042,7 +1042,7 @@ const handleConnectApiKey = Effect.fn("integrations.connectApiKey")(function* ( // Best-effort: add integration bot to org yield* IntegrationBotService.addBotToOrg(provider, orgId).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.logWarning("Failed to add integration bot to org (non-critical)", { event: "integration_bot_add_failed", provider, diff --git a/apps/backend/src/routes/internal.http.ts b/apps/backend/src/routes/internal.http.ts index af50c83da..48d7b2716 100644 --- a/apps/backend/src/routes/internal.http.ts +++ b/apps/backend/src/routes/internal.http.ts @@ -1,4 +1,5 @@ -import { HttpApiBuilder, HttpServerRequest } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpServerRequest } from "effect/unstable/http" import { BotRepo } from "@hazel/backend-core" import { InvalidBearerTokenError, UnauthorizedError } from "@hazel/domain" import { Config, Effect, Option } from "effect" diff --git a/apps/backend/src/routes/klipy.http.ts b/apps/backend/src/routes/klipy.http.ts index facf24f17..14935967c 100644 --- a/apps/backend/src/routes/klipy.http.ts +++ b/apps/backend/src/routes/klipy.http.ts @@ -1,4 +1,5 @@ -import { HttpApiBuilder, HttpClient } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpClient } from "effect/unstable/http" import { KlipyApiError } from "@hazel/domain/http" import { Config, Effect, Redacted, Schema } from "effect" import { HazelApi } from "../api" diff --git a/apps/backend/src/routes/mock-data.http.ts b/apps/backend/src/routes/mock-data.http.ts index 5e4cd2606..7e5cebcdc 100644 --- a/apps/backend/src/routes/mock-data.http.ts +++ b/apps/backend/src/routes/mock-data.http.ts @@ -1,4 +1,4 @@ -import { HttpApiBuilder } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { Database } from "@hazel/db" import { CurrentUser, withRemapDbErrors } from "@hazel/domain" import { OrganizationId, UserId } from "@hazel/schema" diff --git a/apps/backend/src/routes/presence.http.ts b/apps/backend/src/routes/presence.http.ts index 77c3ad6f7..7966ed38b 100644 --- a/apps/backend/src/routes/presence.http.ts +++ b/apps/backend/src/routes/presence.http.ts @@ -1,4 +1,4 @@ -import { HttpApiBuilder } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { UserPresenceStatusRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { withRemapDbErrors } from "@hazel/domain" diff --git a/apps/backend/src/routes/root.http.ts b/apps/backend/src/routes/root.http.ts index 8c54dad7f..a114a6907 100644 --- a/apps/backend/src/routes/root.http.ts +++ b/apps/backend/src/routes/root.http.ts @@ -1,4 +1,4 @@ -import { HttpApiBuilder } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { Effect } from "effect" import { HazelApi } from "../api" diff --git a/apps/backend/src/routes/uploads.http.ts b/apps/backend/src/routes/uploads.http.ts index b1e2f6cbb..6a02f9939 100644 --- a/apps/backend/src/routes/uploads.http.ts +++ b/apps/backend/src/routes/uploads.http.ts @@ -1,4 +1,4 @@ -import { HttpApiBuilder } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { AttachmentRepo, BotRepo, OrganizationRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { CurrentUser, UnauthorizedError, withRemapDbErrors } from "@hazel/domain" diff --git a/apps/backend/src/routes/webhooks.http.ts b/apps/backend/src/routes/webhooks.http.ts index f7bc42321..9e922adcf 100644 --- a/apps/backend/src/routes/webhooks.http.ts +++ b/apps/backend/src/routes/webhooks.http.ts @@ -1,5 +1,6 @@ import { createHmac, timingSafeEqual } from "node:crypto" -import { HttpApiBuilder, HttpServerRequest } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpServerRequest } from "effect/unstable/http" import { GitHubWebhookResponse, InvalidGitHubWebhookSignature } from "@hazel/domain/http" import type { Event } from "@workos-inc/node" import { Config, Effect, pipe, Redacted } from "effect" diff --git a/apps/backend/src/rpc/handlers/channels.ts b/apps/backend/src/rpc/handlers/channels.ts index 624d6e8b9..179da2f0a 100644 --- a/apps/backend/src/rpc/handlers/channels.ts +++ b/apps/backend/src/rpc/handlers/channels.ts @@ -1,4 +1,4 @@ -import { HttpApiClient } from "@effect/platform" +import { HttpApiClient } from "effect/unstable/httpapi" import { ChannelMemberRepo, ChannelRepo, diff --git a/apps/backend/src/rpc/handlers/connect-shares.ts b/apps/backend/src/rpc/handlers/connect-shares.ts index 6b2d9d951..6bdd85053 100644 --- a/apps/backend/src/rpc/handlers/connect-shares.ts +++ b/apps/backend/src/rpc/handlers/connect-shares.ts @@ -163,7 +163,7 @@ export const remapGuestMountInsertConflict = ({ } function remapPermissionError( - effect: Effect.Effect, + make: Effect.Effect, ): Effect.Effect | UnauthorizedError, R> { return Effect.catchIf(effect, PermissionError.is, (err) => Effect.fail( diff --git a/apps/backend/src/rpc/handlers/invitations.ts b/apps/backend/src/rpc/handlers/invitations.ts index fa9a2583e..99e17ce33 100644 --- a/apps/backend/src/rpc/handlers/invitations.ts +++ b/apps/backend/src/rpc/handlers/invitations.ts @@ -87,7 +87,7 @@ export const InvitationRpcLive = InvitationRpcs.toLayer( }).pipe( // Note: catchAll is intentional here for batch processing - // we want to convert ALL errors to InvitationBatchResult entries - Effect.catchAll((error) => + Effect.catch((error) => Effect.succeed( new InvitationBatchResult({ email: invite.email, diff --git a/apps/backend/src/rpc/middleware/auth-class.ts b/apps/backend/src/rpc/middleware/auth-class.ts index 9ec53c147..57f1c2f45 100644 --- a/apps/backend/src/rpc/middleware/auth-class.ts +++ b/apps/backend/src/rpc/middleware/auth-class.ts @@ -8,7 +8,7 @@ * when frontend imports RPC group definitions that reference this middleware. */ -import { RpcMiddleware } from "@effect/rpc" +import { RpcMiddleware } from "effect/unstable/rpc" import { CurrentUser, InvalidBearerTokenError, diff --git a/apps/backend/src/rpc/middleware/auth.test.ts b/apps/backend/src/rpc/middleware/auth.test.ts index be3717e23..fc3f044a9 100644 --- a/apps/backend/src/rpc/middleware/auth.test.ts +++ b/apps/backend/src/rpc/middleware/auth.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, layer } from "@effect/vitest" -import { Headers } from "@effect/platform" +import { Headers } from "effect/unstable/http" import { BotRepo, UserPresenceStatusRepo, UserRepo } from "@hazel/backend-core" import { Effect, Exit, Layer, Option, FiberRef } from "effect" import { AuthMiddleware } from "./auth-class.ts" @@ -100,7 +100,7 @@ const makeAuthMiddlewareLayer = (options?: { status: "offline", customMessage: null, }) as unknown as Effect.Effect - ).pipe(Effect.catchAll(() => Effect.void)) + ).pipe(Effect.catch(() => Effect.void)) } }), ) diff --git a/apps/backend/src/rpc/middleware/auth.ts b/apps/backend/src/rpc/middleware/auth.ts index 665576be9..9c64da775 100644 --- a/apps/backend/src/rpc/middleware/auth.ts +++ b/apps/backend/src/rpc/middleware/auth.ts @@ -1,4 +1,4 @@ -import { Headers } from "@effect/platform" +import { Headers } from "effect/unstable/http" import { BotRepo, UserRepo } from "@hazel/backend-core" import { InvalidBearerTokenError, type CurrentUser, SessionNotProvidedError } from "@hazel/domain" import { Effect, FiberRef, Layer, Option } from "effect" diff --git a/apps/backend/src/rpc/server.ts b/apps/backend/src/rpc/server.ts index 196bdf843..271779f6a 100644 --- a/apps/backend/src/rpc/server.ts +++ b/apps/backend/src/rpc/server.ts @@ -1,4 +1,4 @@ -import { RpcServer } from "@effect/rpc" +import { RpcServer } from "effect/unstable/rpc" import { AttachmentRpcs, BotRpcs, diff --git a/apps/backend/src/services/bot-gateway-service.ts b/apps/backend/src/services/bot-gateway-service.ts index 65d7ea9d6..f5145b784 100644 --- a/apps/backend/src/services/bot-gateway-service.ts +++ b/apps/backend/src/services/bot-gateway-service.ts @@ -6,7 +6,7 @@ import { } from "@hazel/domain" import type { Channel, ChannelMember, Message } from "@hazel/domain/models" import type { BotId, ChannelId, OrganizationId } from "@hazel/schema" -import { Config, Effect, Option, Ref, Schema } from "effect" +import { ServiceMap, Config, Effect, Option, Ref, Schema } from "effect" const DEFAULT_DURABLE_STREAMS_URL = "http://localhost:4437/v1/stream" @@ -20,7 +20,7 @@ const buildStreamPath = (baseUrl: string, botId: BotId): string => const responseText = (response: Response): Promise => response.text().catch(() => `${response.status} ${response.statusText}`) -export class DurableStreamRequestError extends Schema.TaggedError()( +export class DurableStreamRequestError extends Schema.TaggedErrorClass()( "DurableStreamRequestError", { message: Schema.String, @@ -28,10 +28,9 @@ export class DurableStreamRequestError extends Schema.TaggedError()("BotGatewayService", { - accessors: true, +export class BotGatewayService extends ServiceMap.Service()("BotGatewayService", { dependencies: [BotInstallationRepo.Default, ChannelRepo.Default], - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const installationRepo = yield* BotInstallationRepo const channelRepo = yield* ChannelRepo const durableStreamsUrl = yield* Config.string("DURABLE_STREAMS_URL").pipe( diff --git a/apps/backend/src/services/channel-access-sync.ts b/apps/backend/src/services/channel-access-sync.ts index a5fe1fa61..83195967f 100644 --- a/apps/backend/src/services/channel-access-sync.ts +++ b/apps/backend/src/services/channel-access-sync.ts @@ -1,12 +1,11 @@ import { and, eq, isNull, notInArray, schema } from "@hazel/db" import type { ChannelId, ConnectConversationId, OrganizationId, UserId } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { transactionAwareExecute } from "../lib/transaction-aware-execute" -export class ChannelAccessSyncService extends Effect.Service()( +export class ChannelAccessSyncService extends ServiceMap.Service()( "ChannelAccessSyncService", { - accessors: true, effect: Effect.gen(function* () { const upsertChannelUsers = Effect.fn("ChannelAccessSyncService.upsertChannelUsers")(function* ( channelId: ChannelId, diff --git a/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.ts b/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.ts index de7d1aed7..f558ccf1a 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.ts @@ -1,7 +1,7 @@ import { MessageRepo, OrganizationMemberRepo, UserRepo } from "@hazel/backend-core" import type { OrganizationId, UserId } from "@hazel/schema" import type { IntegrationConnection } from "@hazel/domain/models" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" interface ReconcileAttributionParams { organizationId: OrganizationId @@ -14,10 +14,9 @@ interface ReconcileAttributionParams { const defaultShadowDisplayName = (provider: string): string => `${provider.charAt(0).toUpperCase()}${provider.slice(1)} User` -export class ChatSyncAttributionReconciler extends Effect.Service()( +export class ChatSyncAttributionReconciler extends ServiceMap.Service()( "ChatSyncAttributionReconciler", { - accessors: true, effect: Effect.gen(function* () { const messageRepo = yield* MessageRepo const userRepo = yield* UserRepo diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index 8decb7010..cfcd7f80d 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -28,7 +28,7 @@ import { ExternalWebhookId, ExternalThreadId, } from "@hazel/schema" -import { Config, Effect, Option, Redacted, Schema } from "effect" +import { ServiceMap, Config, Effect, Option, Redacted, Schema } from "effect" import { transactionAwareExecute } from "../../lib/transaction-aware-execute" import { ChannelAccessSyncService } from "../channel-access-sync" import { IntegrationBotService } from "../integrations/integration-bot-service" @@ -43,21 +43,21 @@ import { export const DEFAULT_MAX_MESSAGES_PER_CHANNEL = 50 export const DEFAULT_CHAT_SYNC_CONCURRENCY = 5 -export class DiscordSyncConfigurationError extends Schema.TaggedError()( +export class DiscordSyncConfigurationError extends Schema.TaggedErrorClass()( "DiscordSyncConfigurationError", { message: Schema.String, }, ) {} -export class DiscordSyncConnectionNotFoundError extends Schema.TaggedError()( +export class DiscordSyncConnectionNotFoundError extends Schema.TaggedErrorClass()( "DiscordSyncConnectionNotFoundError", { syncConnectionId: SyncConnectionId, }, ) {} -export class DiscordSyncChannelLinkNotFoundError extends Schema.TaggedError()( +export class DiscordSyncChannelLinkNotFoundError extends Schema.TaggedErrorClass()( "DiscordSyncChannelLinkNotFoundError", { syncConnectionId: SyncConnectionId, @@ -65,14 +65,14 @@ export class DiscordSyncChannelLinkNotFoundError extends Schema.TaggedError()( +export class DiscordSyncMessageNotFoundError extends Schema.TaggedErrorClass()( "DiscordSyncMessageNotFoundError", { messageId: MessageId, }, ) {} -export class DiscordSyncApiError extends Schema.TaggedError()("DiscordSyncApiError", { +export class DiscordSyncApiError extends Schema.TaggedErrorClass()("DiscordSyncApiError", { message: Schema.String, status: Schema.optional(Schema.Number), detail: Schema.optional(Schema.String), @@ -147,9 +147,8 @@ export interface ChatSyncIngressThreadCreate { readonly dedupeKey?: string } -export class ChatSyncCoreWorker extends Effect.Service()("ChatSyncCoreWorker", { - accessors: true, - effect: Effect.gen(function* () { +export class ChatSyncCoreWorker extends ServiceMap.Service()("ChatSyncCoreWorker", { + make: Effect.gen(function* () { const db = yield* Database.Database const connectionRepo = yield* ChatSyncConnectionRepo const channelLinkRepo = yield* ChatSyncChannelLinkRepo @@ -530,7 +529,7 @@ export class ChatSyncCoreWorker extends Effect.Service()("Ch ) return Option.some(nextConfig) }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { if (isDiscordApiError(error) && error.status === 403) { const fallbackOutboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings = @@ -636,7 +635,7 @@ export class ChatSyncCoreWorker extends Effect.Service()("Ch return Option.some(outboundMessageId as ExternalMessageId) }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logWarning("Discord webhook send failed; falling back to bot API", { error: String(error), @@ -681,7 +680,7 @@ export class ChatSyncCoreWorker extends Effect.Service()("Ch return true }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logWarning( "Discord webhook update failed; falling back to bot API", @@ -727,7 +726,7 @@ export class ChatSyncCoreWorker extends Effect.Service()("Ch return true }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logWarning( "Discord webhook delete failed; falling back to bot API", diff --git a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts index fce185c27..9a7841fc5 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts @@ -1,18 +1,18 @@ import { Discord } from "@hazel/integrations" -import { Config, Effect, Option, Redacted, Schema, Schedule } from "effect" +import { ServiceMap, Config, Effect, Option, Redacted, Schema, Schedule } from "effect" import { type ChatSyncOutboundAttachment, formatMessageContentWithAttachments, } from "./chat-sync-attachment-content" -export class ChatSyncProviderNotSupportedError extends Schema.TaggedError()( +export class ChatSyncProviderNotSupportedError extends Schema.TaggedErrorClass()( "ChatSyncProviderNotSupportedError", { provider: Schema.String, }, ) {} -export class ChatSyncProviderConfigurationError extends Schema.TaggedError()( +export class ChatSyncProviderConfigurationError extends Schema.TaggedErrorClass()( "ChatSyncProviderConfigurationError", { provider: Schema.String, @@ -20,7 +20,7 @@ export class ChatSyncProviderConfigurationError extends Schema.TaggedError()( +export class ChatSyncProviderApiError extends Schema.TaggedErrorClass()( "ChatSyncProviderApiError", { provider: Schema.String, @@ -85,10 +85,9 @@ const isDiscordSnowflake = (value: string): boolean => value.length >= DISCORD_SNOWFLAKE_MIN_LENGTH && value.length <= DISCORD_SNOWFLAKE_MAX_LENGTH -export class ChatSyncProviderRegistry extends Effect.Service()( +export class ChatSyncProviderRegistry extends ServiceMap.Service()( "ChatSyncProviderRegistry", { - accessors: true, effect: Effect.gen(function* () { const discordApiClient = yield* Discord.DiscordApiClient diff --git a/apps/backend/src/services/chat-sync/discord-gateway-service.ts b/apps/backend/src/services/chat-sync/discord-gateway-service.ts index d2935c140..e5154fa3f 100644 --- a/apps/backend/src/services/chat-sync/discord-gateway-service.ts +++ b/apps/backend/src/services/chat-sync/discord-gateway-service.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto" -import { FetchHttpClient } from "@effect/platform" +import { FetchHttpClient } from "effect/unstable/http" import { BunSocket } from "@effect/platform-bun" import { ChatSyncChannelLinkRepo } from "@hazel/backend-core" import { @@ -12,7 +12,7 @@ import { } from "@hazel/schema" import { DiscordConfig } from "dfx" import { DiscordGateway, DiscordLive } from "dfx/gateway" -import { Config, Effect, Layer, Option, Redacted, Ref, Schema } from "effect" +import { ServiceMap, Config, Effect, Layer, Option, Redacted, Ref, Schema } from "effect" import { DiscordSyncWorker } from "./discord-sync-worker" import type { ChatSyncIngressMessageAttachment } from "./chat-sync-core-worker" @@ -571,9 +571,8 @@ export const createDiscordGatewayDispatchHandlers = (deps: { } } -export class DiscordGatewayService extends Effect.Service()("DiscordGatewayService", { - accessors: true, - effect: Effect.gen(function* () { +export class DiscordGatewayService extends ServiceMap.Service()("DiscordGatewayService", { + make: Effect.gen(function* () { const discordSyncWorker = yield* DiscordSyncWorker const channelLinkRepo = yield* ChatSyncChannelLinkRepo @@ -679,29 +678,29 @@ export class DiscordGatewayService extends Effect.Service [ gateway.handleDispatch("READY", (event) => onReady(event as DiscordReadyEvent).pipe( - Effect.catchAll((error) => onDispatchError("READY", error)), + Effect.catch((error) => onDispatchError("READY", error)), ), ), gateway.handleDispatch("MESSAGE_CREATE", (event) => dispatchHandlers .ingestMessageCreateEvent(event as DiscordMessageCreateEvent) - .pipe(Effect.catchAll((error) => onDispatchError("MESSAGE_CREATE", error))), + .pipe(Effect.catch((error) => onDispatchError("MESSAGE_CREATE", error))), ), gateway.handleDispatch("MESSAGE_UPDATE", (event) => dispatchHandlers .ingestMessageUpdateEvent(event as DiscordMessageUpdateEvent) - .pipe(Effect.catchAll((error) => onDispatchError("MESSAGE_UPDATE", error))), + .pipe(Effect.catch((error) => onDispatchError("MESSAGE_UPDATE", error))), ), gateway.handleDispatch("MESSAGE_DELETE", (event) => dispatchHandlers .ingestMessageDeleteEvent(event as DiscordMessageDeleteEvent) - .pipe(Effect.catchAll((error) => onDispatchError("MESSAGE_DELETE", error))), + .pipe(Effect.catch((error) => onDispatchError("MESSAGE_DELETE", error))), ), gateway.handleDispatch("MESSAGE_REACTION_ADD", (event) => dispatchHandlers .ingestMessageReactionAddEvent(event as DiscordMessageReactionAddEvent) .pipe( - Effect.catchAll((error) => + Effect.catch((error) => onDispatchError("MESSAGE_REACTION_ADD", error), ), ), @@ -710,7 +709,7 @@ export class DiscordGatewayService extends Effect.Service dispatchHandlers .ingestMessageReactionRemoveEvent(event as DiscordMessageReactionRemoveEvent) .pipe( - Effect.catchAll((error) => + Effect.catch((error) => onDispatchError("MESSAGE_REACTION_REMOVE", error), ), ), @@ -718,7 +717,7 @@ export class DiscordGatewayService extends Effect.Service gateway.handleDispatch("THREAD_CREATE", (event) => dispatchHandlers .ingestThreadCreateEvent(event as DiscordThreadCreateEvent) - .pipe(Effect.catchAll((error) => onDispatchError("THREAD_CREATE", error))), + .pipe(Effect.catch((error) => onDispatchError("THREAD_CREATE", error))), ), ], { @@ -728,7 +727,7 @@ export class DiscordGatewayService extends Effect.Service ) }).pipe( Effect.provide(DiscordLayer), - Effect.catchAllCause((cause) => + Effect.catchCause((cause) => Effect.logError("Discord gateway background worker stopped", { cause: String(cause), }), diff --git a/apps/backend/src/services/chat-sync/discord-sync-worker.ts b/apps/backend/src/services/chat-sync/discord-sync-worker.ts index 7ba0cd40a..fe261eb24 100644 --- a/apps/backend/src/services/chat-sync/discord-sync-worker.ts +++ b/apps/backend/src/services/chat-sync/discord-sync-worker.ts @@ -1,4 +1,4 @@ -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { ChannelId, MessageId, MessageReactionId, SyncConnectionId, UserId } from "@hazel/schema" import { DEFAULT_MAX_MESSAGES_PER_CHANNEL, @@ -31,9 +31,8 @@ export { DiscordSyncMessageNotFoundError, } -export class DiscordSyncWorker extends Effect.Service()("DiscordSyncWorker", { - accessors: true, - effect: Effect.gen(function* () { +export class DiscordSyncWorker extends ServiceMap.Service()("DiscordSyncWorker", { + make: Effect.gen(function* () { const coreWorker = yield* ChatSyncCoreWorker const syncConnection = Effect.fn("DiscordSyncWorker.syncConnection")(function* ( diff --git a/apps/backend/src/services/connect-conversation-service.ts b/apps/backend/src/services/connect-conversation-service.ts index cf8f8f44f..5ffcf8ed0 100644 --- a/apps/backend/src/services/connect-conversation-service.ts +++ b/apps/backend/src/services/connect-conversation-service.ts @@ -8,14 +8,13 @@ import { } from "@hazel/backend-core" import { InternalServerError } from "@hazel/domain" import type { ChannelId, ConnectConversationId, OrganizationId, UserId } from "@hazel/schema" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" import { ChannelAccessSyncService } from "./channel-access-sync" import { OrgResolver } from "./org-resolver" -export class ConnectConversationService extends Effect.Service()( +export class ConnectConversationService extends ServiceMap.Service()( "ConnectConversationService", { - accessors: true, dependencies: [ ChannelRepo.Default, ConnectParticipantRepo.Default, diff --git a/apps/backend/src/services/integration-encryption.ts b/apps/backend/src/services/integration-encryption.ts index dbfde45a0..0c4eab7c9 100644 --- a/apps/backend/src/services/integration-encryption.ts +++ b/apps/backend/src/services/integration-encryption.ts @@ -1,4 +1,4 @@ -import { Config, Effect, Option, Redacted, Schema } from "effect" +import { ServiceMap, Config, Effect, Option, Redacted, Schema } from "effect" export interface EncryptedToken { ciphertext: string // Base64 encoded @@ -8,7 +8,7 @@ export interface EncryptedToken { const EncryptionOperation = Schema.Literal("encrypt", "decrypt", "importKey") -export class IntegrationEncryptionError extends Schema.TaggedError()( +export class IntegrationEncryptionError extends Schema.TaggedErrorClass()( "IntegrationEncryptionError", { cause: Schema.Unknown, @@ -16,16 +16,15 @@ export class IntegrationEncryptionError extends Schema.TaggedError()( +export class KeyVersionNotFoundError extends Schema.TaggedErrorClass()( "KeyVersionNotFoundError", { keyVersion: Schema.Number, }, ) {} -export class IntegrationEncryption extends Effect.Service()("IntegrationEncryption", { - accessors: true, - effect: Effect.gen(function* () { +export class IntegrationEncryption extends ServiceMap.Service()("IntegrationEncryption", { + make: Effect.gen(function* () { // Load encryption keys from config (support key rotation) const currentKey = yield* Config.redacted("INTEGRATION_ENCRYPTION_KEY") const currentKeyVersion = yield* Config.number("INTEGRATION_ENCRYPTION_KEY_VERSION").pipe( diff --git a/apps/backend/src/services/integration-token-service.ts b/apps/backend/src/services/integration-token-service.ts index cae9aedbf..5f870d4b9 100644 --- a/apps/backend/src/services/integration-token-service.ts +++ b/apps/backend/src/services/integration-token-service.ts @@ -3,22 +3,22 @@ import type { IntegrationConnectionId, IntegrationTokenId } from "@hazel/schema" import { IntegrationConnection } from "@hazel/domain/models" import { GitHub } from "@hazel/integrations" import { IntegrationConnectionId as IntegrationConnectionIdSchema } from "@hazel/schema" -import { Effect, Option, PartitionedSemaphore, Redacted, Schema } from "effect" +import { ServiceMap, Effect, Option, PartitionedSemaphore, Redacted, Schema } from "effect" import { DatabaseLive } from "./database" import { type EncryptedToken, IntegrationEncryption } from "./integration-encryption" import { OAuthHttpClient } from "./oauth/oauth-http-client" import { type OAuthIntegrationProvider, loadProviderConfig } from "./oauth/provider-config" -export class TokenNotFoundError extends Schema.TaggedError()("TokenNotFoundError", { +export class TokenNotFoundError extends Schema.TaggedErrorClass()("TokenNotFoundError", { connectionId: IntegrationConnectionIdSchema, }) {} -export class TokenRefreshError extends Schema.TaggedError()("TokenRefreshError", { +export class TokenRefreshError extends Schema.TaggedErrorClass()("TokenRefreshError", { provider: IntegrationConnection.IntegrationProvider, cause: Schema.Unknown, }) {} -export class ConnectionNotFoundError extends Schema.TaggedError()( +export class ConnectionNotFoundError extends Schema.TaggedErrorClass()( "ConnectionNotFoundError", { connectionId: IntegrationConnectionIdSchema, @@ -64,10 +64,9 @@ const refreshOAuthToken = ( Effect.mapError((cause) => new TokenRefreshError({ provider, cause })), ) -export class IntegrationTokenService extends Effect.Service()( +export class IntegrationTokenService extends ServiceMap.Service()( "IntegrationTokenService", { - accessors: true, effect: Effect.gen(function* () { const encryption = yield* IntegrationEncryption const tokenRepo = yield* IntegrationTokenRepo diff --git a/apps/backend/src/services/integrations/integration-bot-service.ts b/apps/backend/src/services/integrations/integration-bot-service.ts index 2f254b576..1b6ad62fc 100644 --- a/apps/backend/src/services/integrations/integration-bot-service.ts +++ b/apps/backend/src/services/integrations/integration-bot-service.ts @@ -2,7 +2,7 @@ import { BotInstallationRepo, BotRepo, OrganizationMemberRepo, UserRepo } from " import { Integrations } from "@hazel/domain" import type { OrganizationId } from "@hazel/schema" import type { IntegrationConnection } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" /** * Integration Bot Service @@ -10,9 +10,8 @@ import { Effect, Option } from "effect" * Manages global bot users for integration providers. * Each provider has a single shared bot user across all organizations. */ -export class IntegrationBotService extends Effect.Service()("IntegrationBotService", { - accessors: true, - effect: Effect.gen(function* () { +export class IntegrationBotService extends ServiceMap.Service()("IntegrationBotService", { + make: Effect.gen(function* () { const userRepo = yield* UserRepo const orgMemberRepo = yield* OrganizationMemberRepo const botRepo = yield* BotRepo diff --git a/apps/backend/src/services/message-outbox-dispatcher.test.ts b/apps/backend/src/services/message-outbox-dispatcher.test.ts index 6865ec7b1..7956a7860 100644 --- a/apps/backend/src/services/message-outbox-dispatcher.test.ts +++ b/apps/backend/src/services/message-outbox-dispatcher.test.ts @@ -38,7 +38,7 @@ const runRepoEffect = (harness: ChatSyncDbHarness, effect: Effect.Effec const runDispatcherEffect = ( harness: ChatSyncDbHarness, sideEffects: MessageSideEffectService, - effect: Effect.Effect, + make: Effect.Effect, ) => Effect.runPromise( Effect.scoped( diff --git a/apps/backend/src/services/message-outbox-dispatcher.ts b/apps/backend/src/services/message-outbox-dispatcher.ts index a491b3acc..4ad001673 100644 --- a/apps/backend/src/services/message-outbox-dispatcher.ts +++ b/apps/backend/src/services/message-outbox-dispatcher.ts @@ -9,7 +9,7 @@ import { ReactionDeletedPayloadSchema, } from "@hazel/backend-core/repositories" import { Database } from "@hazel/db" -import { Effect, Redacted, Schema } from "effect" +import { ServiceMap, Effect, Redacted, Schema } from "effect" import { EnvVars } from "../lib/env-vars" import { MessageSideEffectService } from "./message-side-effect-service" @@ -24,10 +24,9 @@ const OUTBOX_DISPATCHER_LOCK_KEY = 1_046_277_921 const computeRetryDelayMs = (attempt: number): number => Math.min(5_000 * 3 ** Math.max(0, attempt - 1), 300_000) -export class MessageOutboxDispatcher extends Effect.Service()( +export class MessageOutboxDispatcher extends ServiceMap.Service()( "MessageOutboxDispatcher", { - accessors: true, dependencies: [EnvVars.Default, MessageOutboxRepo.Default, MessageSideEffectService.Default], effect: Effect.gen(function* () { const envVars = yield* EnvVars @@ -157,7 +156,7 @@ export class MessageOutboxDispatcher extends Effect.Service + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError("Message outbox batch failed", { workerId, @@ -217,7 +216,7 @@ export class MessageOutboxDispatcher extends Effect.Service + Effect.catchCause((cause) => Effect.logError("Message outbox dispatcher leader loop stopped", { workerId, cause: String(cause), diff --git a/apps/backend/src/services/message-side-effect-service.test.ts b/apps/backend/src/services/message-side-effect-service.test.ts index 718379a4e..0c4027cc8 100644 --- a/apps/backend/src/services/message-side-effect-service.test.ts +++ b/apps/backend/src/services/message-side-effect-service.test.ts @@ -1,4 +1,4 @@ -import { FetchHttpClient } from "@effect/platform" +import { FetchHttpClient } from "effect/unstable/http" import { randomUUID } from "node:crypto" import { Database, schema } from "@hazel/db" import type { ChannelId, MessageId, MessageReactionId, OrganizationId, UserId } from "@hazel/schema" @@ -36,7 +36,7 @@ type WorkerOptions = { const runServiceEffect = ( harness: ChatSyncDbHarness, worker: DiscordSyncWorker, - effect: Effect.Effect, + make: Effect.Effect, ) => Effect.runPromise( Effect.scoped( diff --git a/apps/backend/src/services/message-side-effect-service.ts b/apps/backend/src/services/message-side-effect-service.ts index 8fc02a326..6f1a1706c 100644 --- a/apps/backend/src/services/message-side-effect-service.ts +++ b/apps/backend/src/services/message-side-effect-service.ts @@ -1,7 +1,7 @@ -import { HttpApiClient } from "@effect/platform" +import { HttpApiClient } from "effect/unstable/httpapi" import { and, Database, eq, isNull, schema, sql } from "@hazel/db" import { Cluster, WorkflowInitializationError } from "@hazel/domain" -import { Array, Config, Effect, Option } from "effect" +import { ServiceMap, Array, Config, Effect, Option } from "effect" import { TreeFormatter } from "effect/ParseResult" import type { MessageCreatedPayload, @@ -12,10 +12,9 @@ import type { } from "@hazel/backend-core" import { DiscordSyncWorker } from "./chat-sync/discord-sync-worker" -export class MessageSideEffectService extends Effect.Service()( +export class MessageSideEffectService extends ServiceMap.Service()( "MessageSideEffectService", { - accessors: true, dependencies: [DiscordSyncWorker.Default], effect: Effect.gen(function* () { const db = yield* Database.Database @@ -56,7 +55,7 @@ export class MessageSideEffectService extends Effect.Service + Effect.catch((error) => Effect.logWarning("Failed to sync outbox message create to Discord", { messageId: payload.messageId, channelId: payload.channelId, @@ -216,7 +215,7 @@ export class MessageSideEffectService extends Effect.Service + Effect.catch((error) => Effect.logWarning("Failed to sync outbox message update to Discord", { messageId: payload.messageId, error: String(error), @@ -231,7 +230,7 @@ export class MessageSideEffectService extends Effect.Service + Effect.catch((error) => Effect.logWarning("Failed to sync outbox message delete to Discord", { messageId: payload.messageId, error: String(error), @@ -246,7 +245,7 @@ export class MessageSideEffectService extends Effect.Service + Effect.catch((error) => Effect.logWarning("Failed to sync outbox reaction create to Discord", { reactionId: payload.reactionId, error: String(error), @@ -269,7 +268,7 @@ export class MessageSideEffectService extends Effect.Service + Effect.catch((error) => Effect.logWarning("Failed to sync outbox reaction delete to Discord", { hazelMessageId: payload.hazelMessageId, error: String(error), diff --git a/apps/backend/src/services/mock-data-generator.ts b/apps/backend/src/services/mock-data-generator.ts index de8d90d42..775a6c746 100644 --- a/apps/backend/src/services/mock-data-generator.ts +++ b/apps/backend/src/services/mock-data-generator.ts @@ -14,7 +14,7 @@ import type { OrganizationId, UserId, } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { DatabaseLive } from "./database" // Professional team members for a tech startup @@ -178,8 +178,8 @@ interface MockDataConfig { currentUserId: UserId } -export class MockDataGenerator extends Effect.Service()("MockDataGenerator", { - effect: Effect.gen(function* () { +export class MockDataGenerator extends ServiceMap.Service()("MockDataGenerator", { + make: Effect.gen(function* () { const generateForMarketingScreenshots = (config: MockDataConfig) => Effect.gen(function* () { const userRepo = yield* UserRepo diff --git a/apps/backend/src/services/oauth/oauth-http-client.ts b/apps/backend/src/services/oauth/oauth-http-client.ts index 39b80e258..f2c8bf48b 100644 --- a/apps/backend/src/services/oauth/oauth-http-client.ts +++ b/apps/backend/src/services/oauth/oauth-http-client.ts @@ -5,8 +5,8 @@ * Uses HttpClient with proper schema validation and error handling. */ -import { FetchHttpClient, HttpBody, HttpClient } from "@effect/platform" -import { Duration, Effect, Schema } from "effect" +import { FetchHttpClient, HttpBody, HttpClient } from "effect/unstable/http" +import { ServiceMap, Duration, Effect, Schema } from "effect" import { TreeFormatter } from "effect/ParseResult" import type { OAuthIntegrationProvider } from "./provider-config" @@ -32,7 +32,7 @@ const OAuthTokenApiResponse = Schema.Struct({ // Error Types // ============================================================================ -export class OAuthHttpError extends Schema.TaggedError()("OAuthHttpError", { +export class OAuthHttpError extends Schema.TaggedErrorClass()("OAuthHttpError", { message: Schema.String, status: Schema.optional(Schema.Number), cause: Schema.optional(Schema.Unknown), @@ -81,9 +81,8 @@ const encodeFormData = (params: Record): string => * Provides Effect-based HTTP methods for OAuth token operations using HttpClient * with proper schema validation and error handling. */ -export class OAuthHttpClient extends Effect.Service()("OAuthHttpClient", { - accessors: true, - effect: Effect.gen(function* () { +export class OAuthHttpClient extends ServiceMap.Service()("OAuthHttpClient", { + make: Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient /** diff --git a/apps/backend/src/services/oauth/oauth-provider-registry.ts b/apps/backend/src/services/oauth/oauth-provider-registry.ts index c6e6536f8..79a731480 100644 --- a/apps/backend/src/services/oauth/oauth-provider-registry.ts +++ b/apps/backend/src/services/oauth/oauth-provider-registry.ts @@ -1,6 +1,6 @@ import { UnsupportedProviderError } from "@hazel/domain/http" import { GitHub } from "@hazel/integrations" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import type { OAuthProvider } from "./oauth-provider" import { ProviderNotConfiguredError } from "./oauth-provider" import type { IntegrationProvider, OAuthIntegrationProvider, OAuthProviderConfig } from "./provider-config" @@ -64,9 +64,8 @@ const SUPPORTED_PROVIDERS: readonly OAuthIntegrationProvider[] = ["linear", "git * 3. Add provider to SUPPORTED_PROVIDERS array * 4. Set environment variables: {PROVIDER}_CLIENT_ID, {PROVIDER}_CLIENT_SECRET, {PROVIDER}_REDIRECT_URI */ -export class OAuthProviderRegistry extends Effect.Service()("OAuthProviderRegistry", { - accessors: true, - effect: Effect.gen(function* () { +export class OAuthProviderRegistry extends ServiceMap.Service()("OAuthProviderRegistry", { + make: Effect.gen(function* () { // Cache for loaded providers const providerCache = new Map() diff --git a/apps/backend/src/services/oauth/oauth-provider.ts b/apps/backend/src/services/oauth/oauth-provider.ts index 0ce97fd57..5ede165fb 100644 --- a/apps/backend/src/services/oauth/oauth-provider.ts +++ b/apps/backend/src/services/oauth/oauth-provider.ts @@ -13,7 +13,7 @@ const IntegrationProviderSchema = Schema.Literal("linear", "github", "figma", "n /** * Error when exchanging authorization code for tokens fails. */ -export class TokenExchangeError extends Schema.TaggedError()("TokenExchangeError", { +export class TokenExchangeError extends Schema.TaggedErrorClass()("TokenExchangeError", { provider: IntegrationProviderSchema, message: Schema.String, cause: Schema.optional(Schema.Unknown), @@ -22,7 +22,7 @@ export class TokenExchangeError extends Schema.TaggedError() /** * Error when fetching account info from provider fails. */ -export class AccountInfoError extends Schema.TaggedError()("AccountInfoError", { +export class AccountInfoError extends Schema.TaggedErrorClass()("AccountInfoError", { provider: IntegrationProviderSchema, message: Schema.String, cause: Schema.optional(Schema.Unknown), @@ -31,7 +31,7 @@ export class AccountInfoError extends Schema.TaggedError()("Ac /** * Error when refreshing access token fails. */ -export class TokenRefreshError extends Schema.TaggedError()("TokenRefreshError", { +export class TokenRefreshError extends Schema.TaggedErrorClass()("TokenRefreshError", { provider: IntegrationProviderSchema, message: Schema.String, cause: Schema.optional(Schema.Unknown), @@ -40,7 +40,7 @@ export class TokenRefreshError extends Schema.TaggedError()(" /** * Error when provider is not supported or not configured. */ -export class ProviderNotConfiguredError extends Schema.TaggedError()( +export class ProviderNotConfiguredError extends Schema.TaggedErrorClass()( "ProviderNotConfiguredError", { provider: IntegrationProviderSchema, diff --git a/apps/backend/src/services/org-resolver.test.ts b/apps/backend/src/services/org-resolver.test.ts index 3b3e06204..d697f2173 100644 --- a/apps/backend/src/services/org-resolver.test.ts +++ b/apps/backend/src/services/org-resolver.test.ts @@ -69,7 +69,7 @@ const makeResolverLayer = (opts: { ) const runEither = ( - effect: Effect.Effect, + make: Effect.Effect, layer: Layer.Layer, actor: CurrentUser.Schema = makeActor(), ) => diff --git a/apps/backend/src/services/org-resolver.ts b/apps/backend/src/services/org-resolver.ts index 085b6e079..8f88aebbb 100644 --- a/apps/backend/src/services/org-resolver.ts +++ b/apps/backend/src/services/org-resolver.ts @@ -3,7 +3,7 @@ import { PermissionError } from "@hazel/domain" import * as CurrentUser from "@hazel/domain/current-user" import { type ApiScope, CurrentBotScopes, scopesForRole } from "@hazel/domain/scopes" import type { ChannelId, MessageId, OrganizationId } from "@hazel/schema" -import { Effect, FiberRef, Option } from "effect" +import { ServiceMap, Effect, FiberRef, Option } from "effect" import { isAdminOrOwner, type OrganizationRole } from "../lib/policy-utils" /** @@ -11,8 +11,8 @@ import { isAdminOrOwner, type OrganizationRole } from "../lib/policy-utils" * It replaces ad-hoc role checks across individual policy services with a single * point of scope resolution. */ -export class OrgResolver extends Effect.Service()("OrgResolver", { - effect: Effect.gen(function* () { +export class OrgResolver extends ServiceMap.Service()("OrgResolver", { + make: Effect.gen(function* () { const organizationMemberRepo = yield* OrganizationMemberRepo const channelRepo = yield* ChannelRepo const channelMemberRepo = yield* ChannelMemberRepo @@ -253,5 +253,4 @@ export class OrgResolver extends Effect.Service()("OrgResolver", { ChannelMemberRepo.Default, MessageRepo.Default, ], - accessors: true, }) {} diff --git a/apps/backend/src/services/rate-limiter.ts b/apps/backend/src/services/rate-limiter.ts index 7be8810b4..88d41b653 100644 --- a/apps/backend/src/services/rate-limiter.ts +++ b/apps/backend/src/services/rate-limiter.ts @@ -1,5 +1,5 @@ import { Redis, type RedisErrors } from "@hazel/effect-bun" -import { Effect, Layer, Schema } from "effect" +import { ServiceMap, Effect, Layer, Schema } from "effect" /** * Result of a rate limit check @@ -15,7 +15,7 @@ export interface RateLimitResult { readonly limit: number } -export class RateLimiterError extends Schema.TaggedError()("RateLimiterError", { +export class RateLimiterError extends Schema.TaggedErrorClass()("RateLimiterError", { message: Schema.String, cause: Schema.optional(Schema.Unknown), }) {} @@ -58,9 +58,9 @@ end /** * Rate limiter service backed by Redis via @hazel/effect-bun */ -export class RateLimiter extends Effect.Service()("RateLimiter", { +export class RateLimiter extends ServiceMap.Service()("RateLimiter", { dependencies: [Redis.Default], - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const redis = yield* Redis return { diff --git a/apps/backend/src/services/session-manager.ts b/apps/backend/src/services/session-manager.ts index ba7ab5097..43f2498ee 100644 --- a/apps/backend/src/services/session-manager.ts +++ b/apps/backend/src/services/session-manager.ts @@ -6,7 +6,7 @@ import { WorkOSUserFetchError, } from "@hazel/domain" import { UserRepo } from "@hazel/backend-core" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" /** * Session management service that handles authentication via WorkOS. @@ -14,10 +14,9 @@ import { Effect } from "effect" * * This service delegates to @hazel/auth/backend for the actual authentication logic. */ -export class SessionManager extends Effect.Service()("SessionManager", { - accessors: true, +export class SessionManager extends ServiceMap.Service()("SessionManager", { dependencies: [BackendAuth.Default, UserRepo.Default], - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const auth = yield* BackendAuth const userRepo = yield* UserRepo diff --git a/apps/backend/src/services/webhook-bot-service.ts b/apps/backend/src/services/webhook-bot-service.ts index 32e499471..bbfef4367 100644 --- a/apps/backend/src/services/webhook-bot-service.ts +++ b/apps/backend/src/services/webhook-bot-service.ts @@ -1,6 +1,6 @@ import { OrganizationMemberRepo, UserRepo } from "@hazel/backend-core" import type { ChannelWebhookId, OrganizationId, UserId } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" /** * Webhook Bot Service @@ -8,9 +8,8 @@ import { Effect } from "effect" * Manages machine users for channel webhooks. * Each webhook has its own unique bot user identity. */ -export class WebhookBotService extends Effect.Service()("WebhookBotService", { - accessors: true, - effect: Effect.gen(function* () { +export class WebhookBotService extends ServiceMap.Service()("WebhookBotService", { + make: Effect.gen(function* () { const userRepo = yield* UserRepo const orgMemberRepo = yield* OrganizationMemberRepo diff --git a/apps/backend/src/services/workos-auth.ts b/apps/backend/src/services/workos-auth.ts index 8c02431c7..6a31f0205 100644 --- a/apps/backend/src/services/workos-auth.ts +++ b/apps/backend/src/services/workos-auth.ts @@ -1,13 +1,12 @@ import { WorkOS as WorkOSNodeAPI } from "@workos-inc/node" -import { Config, Effect, Redacted, Schema } from "effect" +import { ServiceMap, Config, Effect, Redacted, Schema } from "effect" -export class WorkOSAuthError extends Schema.TaggedError()("WorkOSAuthError", { +export class WorkOSAuthError extends Schema.TaggedErrorClass()("WorkOSAuthError", { cause: Schema.Unknown, }) {} -export class WorkOSAuth extends Effect.Service()("WorkOSAuth", { - accessors: true, - effect: Effect.gen(function* () { +export class WorkOSAuth extends ServiceMap.Service()("WorkOSAuth", { + make: Effect.gen(function* () { const apiKey = yield* Config.redacted("WORKOS_API_KEY") const clientId = yield* Config.string("WORKOS_CLIENT_ID") diff --git a/apps/backend/src/services/workos-webhook.ts b/apps/backend/src/services/workos-webhook.ts index 76a6b281f..49c80f1f7 100644 --- a/apps/backend/src/services/workos-webhook.ts +++ b/apps/backend/src/services/workos-webhook.ts @@ -1,14 +1,14 @@ import * as crypto from "node:crypto" -import { Config, DateTime, Duration, Effect, Schema } from "effect" +import { ServiceMap, Config, DateTime, Duration, Effect, Schema } from "effect" // Error types -export class WebhookVerificationError extends Schema.TaggedError( +export class WebhookVerificationError extends Schema.TaggedErrorClass( "WebhookVerificationError", )("WebhookVerificationError", { message: Schema.String, }) {} -export class WebhookTimestampError extends Schema.TaggedError("WebhookTimestampError")( +export class WebhookTimestampError extends Schema.TaggedErrorClass("WebhookTimestampError")( "WebhookTimestampError", { message: Schema.String, @@ -24,9 +24,8 @@ export interface WorkOSWebhookSignature { const DEFAULT_TIMESTAMP_TOLERANCE = Duration.minutes(5) -export class WorkOSWebhookVerifier extends Effect.Service()("WorkOSWebhookVerifier", { - accessors: true, - effect: Effect.gen(function* () { +export class WorkOSWebhookVerifier extends ServiceMap.Service()("WorkOSWebhookVerifier", { + make: Effect.gen(function* () { // Get webhook secret from config const webhookSecret = yield* Config.string("WORKOS_WEBHOOK_SECRET") diff --git a/apps/bot-gateway/package.json b/apps/bot-gateway/package.json index d86cc6f4d..359640f88 100644 --- a/apps/bot-gateway/package.json +++ b/apps/bot-gateway/package.json @@ -18,7 +18,6 @@ "effect": "catalog:effect" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3" } diff --git a/apps/bot-gateway/src/index.ts b/apps/bot-gateway/src/index.ts index 8de33d00f..59387c38c 100644 --- a/apps/bot-gateway/src/index.ts +++ b/apps/bot-gateway/src/index.ts @@ -120,9 +120,8 @@ const extractGatewayOp = (payload: string | BufferSource): string | undefined => } } -export class GatewayConfig extends Effect.Service()("GatewayConfig", { - accessors: true, - effect: Effect.gen(function* () { +export class GatewayConfig extends ServiceMap.Service()("GatewayConfig", { + make: Effect.gen(function* () { const config = { port: yield* Config.integer("PORT").pipe(Config.withDefault(DEFAULT_PORT)), isDev: yield* Config.boolean("IS_DEV").pipe(Config.withDefault(false)), @@ -149,21 +148,21 @@ export class GatewayConfig extends Effect.Service()("GatewayConfi }), }) {} -class GatewayAuthError extends Schema.TaggedError()("GatewayAuthError", { +class GatewayAuthError extends Schema.TaggedErrorClass()("GatewayAuthError", { message: Schema.String, }) {} -class GatewayProtocolError extends Schema.TaggedError()("GatewayProtocolError", { +class GatewayProtocolError extends Schema.TaggedErrorClass()("GatewayProtocolError", { message: Schema.String, }) {} -export class GatewayStartupError extends Schema.TaggedError()("GatewayStartupError", { +export class GatewayStartupError extends Schema.TaggedErrorClass()("GatewayStartupError", { dependency: Schema.Literal("config", "database", "redis", "tracer", "server"), message: Schema.String, cause: Schema.optional(Schema.Unknown), }) {} -class DurableStreamGatewayError extends Schema.TaggedError()( +class DurableStreamGatewayError extends Schema.TaggedErrorClass()( "DurableStreamGatewayError", { message: Schema.String, @@ -191,9 +190,8 @@ interface GatewaySession { closed: boolean } -class DurableStreamClient extends Effect.Service()("DurableStreamClient", { - accessors: true, - effect: Effect.gen(function* () { +class DurableStreamClient extends ServiceMap.Service()("DurableStreamClient", { + make: Effect.gen(function* () { const config = yield* GatewayConfig const ensuredStreamsRef = yield* Ref.make(new Set()) const authHeaders: Record = Option.isSome(config.durableStreamsToken) @@ -324,9 +322,8 @@ class DurableStreamClient extends Effect.Service()("Durable }), }) {} -class BotGatewayHub extends Effect.Service()("BotGatewayHub", { - accessors: true, - effect: Effect.gen(function* () { +class BotGatewayHub extends ServiceMap.Service()("BotGatewayHub", { + make: Effect.gen(function* () { const botRepo = yield* BotRepo const redis = yield* Redis const durableStreams = yield* DurableStreamClient @@ -747,9 +744,9 @@ class BotGatewayHub extends Effect.Service()("BotGatewayHub", { new GatewayProtocolError({ message: `Session ${id} closed before ACK`, }), - ).pipe(Effect.catchAll(() => Effect.void)) + ).pipe(Effect.catch(() => Effect.void)) } - yield* releaseLease(session.botId, id).pipe(Effect.catchAll(() => Effect.void)) + yield* releaseLease(session.botId, id).pipe(Effect.catch(() => Effect.void)) yield* deleteSession(id) }) diff --git a/apps/cluster/package.json b/apps/cluster/package.json index 3d3bbbd31..0c882235f 100644 --- a/apps/cluster/package.json +++ b/apps/cluster/package.json @@ -9,13 +9,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@effect/ai": "catalog:effect", - "@effect/cluster": "catalog:effect", - "@effect/experimental": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-bun": "catalog:effect", "@effect/sql-pg": "catalog:effect", - "@effect/workflow": "catalog:effect", "@hazel/ai-openrouter": "workspace:*", "@hazel/backend-core": "workspace:*", "@hazel/db": "workspace:*", diff --git a/apps/cluster/src/cron/presence-cleanup-cron.ts b/apps/cluster/src/cron/presence-cleanup-cron.ts index bce73cb49..d16e47ac8 100644 --- a/apps/cluster/src/cron/presence-cleanup-cron.ts +++ b/apps/cluster/src/cron/presence-cleanup-cron.ts @@ -1,4 +1,4 @@ -import * as ClusterCron from "@effect/cluster/ClusterCron" +import * as ClusterCron from "effect/unstable/cluster/ClusterCron" import { and, Database, inArray, lt, ne, schema } from "@hazel/db" import * as Cron from "effect/Cron" import * as Duration from "effect/Duration" diff --git a/apps/cluster/src/cron/rss-poll-cron.ts b/apps/cluster/src/cron/rss-poll-cron.ts index 070af1a32..46ef37039 100644 --- a/apps/cluster/src/cron/rss-poll-cron.ts +++ b/apps/cluster/src/cron/rss-poll-cron.ts @@ -1,5 +1,5 @@ -import * as ClusterCron from "@effect/cluster/ClusterCron" -import { WorkflowEngine } from "@effect/workflow" +import * as ClusterCron from "effect/unstable/cluster/ClusterCron" +import { WorkflowEngine } from "effect/unstable/workflow" import { and, Database, eq, isNull, lt, schema, sql } from "@hazel/db" import { Cluster } from "@hazel/domain" import * as Cron from "effect/Cron" @@ -81,7 +81,7 @@ export const RssPollCronLayer = ClusterCron.make({ discard: true, }) .pipe( - Effect.catchAll((err) => + Effect.catch((err) => Effect.gen(function* () { yield* Effect.logWarning( `Failed to execute RssFeedPollWorkflow for subscription ${sub.id}`, @@ -104,7 +104,7 @@ export const RssPollCronLayer = ClusterCron.make({ .where(eq(schema.rssSubscriptionsTable.id, sub.id)), ) .pipe( - Effect.catchAll((dbErr) => + Effect.catch((dbErr) => Effect.logWarning("Failed to increment RSS error counter", { subscriptionId: sub.id, error: String(dbErr), diff --git a/apps/cluster/src/cron/status-expiration-cron.ts b/apps/cluster/src/cron/status-expiration-cron.ts index 95258847b..711141ebb 100644 --- a/apps/cluster/src/cron/status-expiration-cron.ts +++ b/apps/cluster/src/cron/status-expiration-cron.ts @@ -1,4 +1,4 @@ -import * as ClusterCron from "@effect/cluster/ClusterCron" +import * as ClusterCron from "effect/unstable/cluster/ClusterCron" import { and, Database, isNotNull, lt, schema } from "@hazel/db" import * as Cron from "effect/Cron" import * as Duration from "effect/Duration" diff --git a/apps/cluster/src/cron/typing-indicator-cleanup-cron.ts b/apps/cluster/src/cron/typing-indicator-cleanup-cron.ts index a68a3e7fb..dfb7364c7 100644 --- a/apps/cluster/src/cron/typing-indicator-cleanup-cron.ts +++ b/apps/cluster/src/cron/typing-indicator-cleanup-cron.ts @@ -1,4 +1,4 @@ -import * as ClusterCron from "@effect/cluster/ClusterCron" +import * as ClusterCron from "effect/unstable/cluster/ClusterCron" import { Database, lt, schema } from "@hazel/db" import * as Cron from "effect/Cron" import * as Duration from "effect/Duration" diff --git a/apps/cluster/src/cron/upload-cleanup-cron.ts b/apps/cluster/src/cron/upload-cleanup-cron.ts index 3b8558e69..5f6606451 100644 --- a/apps/cluster/src/cron/upload-cleanup-cron.ts +++ b/apps/cluster/src/cron/upload-cleanup-cron.ts @@ -1,4 +1,4 @@ -import * as ClusterCron from "@effect/cluster/ClusterCron" +import * as ClusterCron from "effect/unstable/cluster/ClusterCron" import { and, Database, eq, isNull, lt, schema } from "@hazel/db" import * as Cron from "effect/Cron" import * as Duration from "effect/Duration" diff --git a/apps/cluster/src/cron/workos-sync-cron.ts b/apps/cluster/src/cron/workos-sync-cron.ts index 74bcdf6f4..4bf932f62 100644 --- a/apps/cluster/src/cron/workos-sync-cron.ts +++ b/apps/cluster/src/cron/workos-sync-cron.ts @@ -1,4 +1,4 @@ -import * as ClusterCron from "@effect/cluster/ClusterCron" +import * as ClusterCron from "effect/unstable/cluster/ClusterCron" import { WorkOSSync } from "@hazel/backend-core/services" import * as Cron from "effect/Cron" import * as Duration from "effect/Duration" diff --git a/apps/cluster/src/index.ts b/apps/cluster/src/index.ts index 57f2f7613..2eb58250e 100644 --- a/apps/cluster/src/index.ts +++ b/apps/cluster/src/index.ts @@ -1,8 +1,9 @@ -import { ClusterWorkflowEngine } from "@effect/cluster" -import { HttpApiBuilder, HttpMiddleware, HttpServer } from "@effect/platform" +import { ClusterWorkflowEngine } from "effect/unstable/cluster" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpMiddleware, HttpServer } from "effect/unstable/http" import { BunClusterSocket, BunHttpServer, BunRuntime } from "@effect/platform-bun" import { PgClient } from "@effect/sql-pg" -import { WorkflowProxyServer } from "@effect/workflow" +import { WorkflowProxyServer } from "effect/unstable/workflow" import { InvitationRepo, OrganizationMemberRepo, diff --git a/apps/cluster/src/services/bot-user-service.ts b/apps/cluster/src/services/bot-user-service.ts index 1dd4eb2bd..38bd61c9a 100644 --- a/apps/cluster/src/services/bot-user-service.ts +++ b/apps/cluster/src/services/bot-user-service.ts @@ -1,7 +1,7 @@ import { Database, eq, schema } from "@hazel/db" import { Cluster, Integrations } from "@hazel/domain" import type { UserId } from "@hazel/schema" -import { Array, Effect, Layer, Option } from "effect" +import { ServiceMap, Array, Effect, Layer, Option } from "effect" /** * Service for cached bot user lookups. @@ -10,9 +10,8 @@ import { Array, Effect, Layer, Option } from "effect" * This service caches the bot user ID at initialization to avoid * repeated database queries on every webhook. */ -export class BotUserService extends Effect.Service()("BotUserService", { - accessors: true, - effect: Effect.gen(function* () { +export class BotUserService extends ServiceMap.Service()("BotUserService", { + make: Effect.gen(function* () { const db = yield* Database.Database // Cache for bot user IDs by external ID diff --git a/apps/cluster/src/services/openrouter-service.ts b/apps/cluster/src/services/openrouter-service.ts index 4f5988beb..78ef1bfc5 100644 --- a/apps/cluster/src/services/openrouter-service.ts +++ b/apps/cluster/src/services/openrouter-service.ts @@ -1,5 +1,5 @@ import { OpenRouterClient, OpenRouterLanguageModel } from "@hazel/ai-openrouter" -import { FetchHttpClient } from "@effect/platform" +import { FetchHttpClient } from "effect/unstable/http" import { Config, Layer } from "effect" // OpenRouter configuration from environment diff --git a/apps/cluster/src/workflows/cleanup-uploads-handler.ts b/apps/cluster/src/workflows/cleanup-uploads-handler.ts index 09998c42e..02a8f3840 100644 --- a/apps/cluster/src/workflows/cleanup-uploads-handler.ts +++ b/apps/cluster/src/workflows/cleanup-uploads-handler.ts @@ -1,4 +1,4 @@ -import { Activity } from "@effect/workflow" +import { Activity } from "effect/unstable/workflow" import { and, Database, eq, isNull, lt, schema } from "@hazel/db" import { Cluster } from "@hazel/domain" import type { AttachmentId } from "@hazel/schema" diff --git a/apps/cluster/src/workflows/github-installation-handler.ts b/apps/cluster/src/workflows/github-installation-handler.ts index 4a2051d02..adc50b76c 100644 --- a/apps/cluster/src/workflows/github-installation-handler.ts +++ b/apps/cluster/src/workflows/github-installation-handler.ts @@ -1,4 +1,4 @@ -import { Activity } from "@effect/workflow" +import { Activity } from "effect/unstable/workflow" import { and, Database, eq, isNull, schema, sql } from "@hazel/db" import { Cluster } from "@hazel/domain" import { Effect } from "effect" diff --git a/apps/cluster/src/workflows/github-webhook-handler.ts b/apps/cluster/src/workflows/github-webhook-handler.ts index 9dba2e7ab..52463320a 100644 --- a/apps/cluster/src/workflows/github-webhook-handler.ts +++ b/apps/cluster/src/workflows/github-webhook-handler.ts @@ -1,4 +1,4 @@ -import { Activity } from "@effect/workflow" +import { Activity } from "effect/unstable/workflow" import { and, Database, eq, isNull, schema } from "@hazel/db" import { Cluster } from "@hazel/domain" import type { MessageId } from "@hazel/schema" diff --git a/apps/cluster/src/workflows/message-notification-handler.ts b/apps/cluster/src/workflows/message-notification-handler.ts index 4bae8ad1d..ca7a8a52b 100644 --- a/apps/cluster/src/workflows/message-notification-handler.ts +++ b/apps/cluster/src/workflows/message-notification-handler.ts @@ -1,4 +1,4 @@ -import { Activity } from "@effect/workflow" +import { Activity } from "effect/unstable/workflow" import { and, Database, eq, inArray, isNull, ne, or, schema, sql } from "@hazel/db" import { Cluster } from "@hazel/domain" import type { ChannelMemberId, NotificationId, OrganizationMemberId, UserId } from "@hazel/schema" diff --git a/apps/cluster/src/workflows/rss-feed-poll-handler.ts b/apps/cluster/src/workflows/rss-feed-poll-handler.ts index e68a24029..701d3c7b0 100644 --- a/apps/cluster/src/workflows/rss-feed-poll-handler.ts +++ b/apps/cluster/src/workflows/rss-feed-poll-handler.ts @@ -1,4 +1,4 @@ -import { Activity } from "@effect/workflow" +import { Activity } from "effect/unstable/workflow" import { and, Database, eq, isNull, schema } from "@hazel/db" import { Cluster } from "@hazel/domain" import type { MessageId } from "@hazel/schema" @@ -239,7 +239,7 @@ export const RssFeedPollWorkflowLayer = Cluster.RssFeedPollWorkflow.toLayer( }), ) .pipe( - Effect.catchAll((err) => + Effect.catch((err) => Effect.logWarning( "Failed to record posted item (may cause duplicate on next poll)", { error: err, itemGuid: item.guid }, diff --git a/apps/cluster/src/workflows/thread-naming-handler.ts b/apps/cluster/src/workflows/thread-naming-handler.ts index 8df040a30..c892251d0 100644 --- a/apps/cluster/src/workflows/thread-naming-handler.ts +++ b/apps/cluster/src/workflows/thread-naming-handler.ts @@ -1,5 +1,5 @@ -import { LanguageModel } from "@effect/ai" -import { Activity } from "@effect/workflow" +import { LanguageModel } from "effect/unstable/ai" +import { Activity } from "effect/unstable/workflow" import { and, Database, eq, isNull, schema } from "@hazel/db" import { Cluster } from "@hazel/domain" import { Effect } from "effect" diff --git a/apps/electric-proxy/package.json b/apps/electric-proxy/package.json index 174af38a4..7f146453d 100644 --- a/apps/electric-proxy/package.json +++ b/apps/electric-proxy/package.json @@ -9,8 +9,6 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@effect/experimental": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-bun": "catalog:effect", "@electric-sql/client": "1.5.12", "@hazel/auth": "workspace:*", diff --git a/apps/electric-proxy/src/auth/bot-auth.ts b/apps/electric-proxy/src/auth/bot-auth.ts index 2e9a0bd88..66130e3bc 100644 --- a/apps/electric-proxy/src/auth/bot-auth.ts +++ b/apps/electric-proxy/src/auth/bot-auth.ts @@ -18,7 +18,7 @@ export interface AuthenticatedBot { /** * Bot authentication error */ -export class BotAuthenticationError extends Schema.TaggedError( +export class BotAuthenticationError extends Schema.TaggedErrorClass( "BotAuthenticationError", )("BotAuthenticationError", { message: Schema.String, diff --git a/apps/electric-proxy/src/cache/access-context-cache.ts b/apps/electric-proxy/src/cache/access-context-cache.ts index 535ef353a..a30c51109 100644 --- a/apps/electric-proxy/src/cache/access-context-cache.ts +++ b/apps/electric-proxy/src/cache/access-context-cache.ts @@ -23,7 +23,7 @@ export type BotAccessContext = { /** * Cache lookup error - when we fail to fetch from database */ -export class AccessContextLookupError extends Schema.TaggedError()( +export class AccessContextLookupError extends Schema.TaggedErrorClass()( "AccessContextLookupError", { message: Schema.String, diff --git a/apps/electric-proxy/src/cache/access-context-service.ts b/apps/electric-proxy/src/cache/access-context-service.ts index 1827dc3b3..c1a6ab5e7 100644 --- a/apps/electric-proxy/src/cache/access-context-service.ts +++ b/apps/electric-proxy/src/cache/access-context-service.ts @@ -1,7 +1,7 @@ -import { PersistedCache, type Persistence } from "@effect/experimental" +import { PersistedCache, type Persistence } from "effect/unstable/persistence" import { and, Database, eq, isNull, schema } from "@hazel/db" import type { BotId, ChannelId, UserId } from "@hazel/schema" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { AccessContextLookupError, type BotAccessContext, @@ -32,10 +32,9 @@ export interface AccessContextCache { * Note: Database.Database is intentionally NOT included in dependencies * as it's a global infrastructure layer provided at the application root. */ -export class AccessContextCacheService extends Effect.Service()( +export class AccessContextCacheService extends ServiceMap.Service()( "AccessContextCacheService", { - accessors: true, scoped: Effect.gen(function* () { const db = yield* Database.Database diff --git a/apps/electric-proxy/src/cache/redis-persistence.ts b/apps/electric-proxy/src/cache/redis-persistence.ts index c70e59f30..07030ab55 100644 --- a/apps/electric-proxy/src/cache/redis-persistence.ts +++ b/apps/electric-proxy/src/cache/redis-persistence.ts @@ -1,4 +1,4 @@ -import { Persistence } from "@effect/experimental" +import { Persistence } from "effect/unstable/persistence" import { Redis, RedisResultPersistenceLive } from "@hazel/effect-bun" import { Effect, Layer, Redacted } from "effect" import { ProxyConfigService } from "../config" diff --git a/apps/electric-proxy/src/config.ts b/apps/electric-proxy/src/config.ts index a1823ede2..cc3683128 100644 --- a/apps/electric-proxy/src/config.ts +++ b/apps/electric-proxy/src/config.ts @@ -1,4 +1,4 @@ -import { Config, Effect, Option, Redacted } from "effect" +import { ServiceMap, Config, Effect, Option, Redacted } from "effect" /** * Proxy configuration interface @@ -21,9 +21,8 @@ export interface ProxyConfig { * Proxy configuration service. * Reads configuration from environment variables. */ -export class ProxyConfigService extends Effect.Service()("ProxyConfigService", { - accessors: true, - effect: Effect.gen(function* () { +export class ProxyConfigService extends ServiceMap.Service()("ProxyConfigService", { + make: Effect.gen(function* () { const electricUrl = yield* Config.string("ELECTRIC_URL") const electricSourceId = yield* Config.string("ELECTRIC_SOURCE_ID").pipe( Config.option, diff --git a/apps/electric-proxy/src/index.ts b/apps/electric-proxy/src/index.ts index 1b2ed276d..a27841ed3 100644 --- a/apps/electric-proxy/src/index.ts +++ b/apps/electric-proxy/src/index.ts @@ -180,7 +180,7 @@ const handleUserRequest = (request: Request) => { }), ), // Fallback for any unhandled errors - returns error details to client for debugging - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { const errorTag = (error as { _tag?: string })?._tag ?? "UnknownError" yield* annotateHandledError(500, errorTag) @@ -373,7 +373,7 @@ const handleBotRequest = (request: Request) => { }), ), // Fallback for any unhandled errors - returns error details to client for debugging - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { const errorTag = (error as { _tag?: string })?._tag ?? "UnknownError" yield* annotateHandledError(500, errorTag) diff --git a/apps/electric-proxy/src/proxy/electric-client.ts b/apps/electric-proxy/src/proxy/electric-client.ts index 81e1242d3..d802984d5 100644 --- a/apps/electric-proxy/src/proxy/electric-client.ts +++ b/apps/electric-proxy/src/proxy/electric-client.ts @@ -6,7 +6,7 @@ import { proxyElectricDuration, proxyElectricErrors } from "../observability/met /** * Error thrown when Electric proxy request fails */ -export class ElectricProxyError extends Schema.TaggedError()("ElectricProxyError", { +export class ElectricProxyError extends Schema.TaggedErrorClass()("ElectricProxyError", { message: Schema.String, detail: Schema.optional(Schema.String), }) {} diff --git a/apps/electric-proxy/src/tables/bot-tables.ts b/apps/electric-proxy/src/tables/bot-tables.ts index 0e203c0f7..99ad37377 100644 --- a/apps/electric-proxy/src/tables/bot-tables.ts +++ b/apps/electric-proxy/src/tables/bot-tables.ts @@ -6,7 +6,7 @@ import type { WhereClauseResult } from "./where-clause-builder" /** * Error thrown when bot table access is denied or where clause cannot be generated */ -export class BotTableAccessError extends Schema.TaggedError()("BotTableAccessError", { +export class BotTableAccessError extends Schema.TaggedErrorClass()("BotTableAccessError", { message: Schema.String, detail: Schema.optional(Schema.String), table: Schema.String, diff --git a/apps/electric-proxy/src/tables/user-tables.ts b/apps/electric-proxy/src/tables/user-tables.ts index b5b25c9ea..f9eddc9c6 100644 --- a/apps/electric-proxy/src/tables/user-tables.ts +++ b/apps/electric-proxy/src/tables/user-tables.ts @@ -15,7 +15,7 @@ import { /** * Error thrown when table access is denied or where clause cannot be generated */ -export class TableAccessError extends Schema.TaggedError()("TableAccessError", { +export class TableAccessError extends Schema.TaggedErrorClass()("TableAccessError", { message: Schema.String, detail: Schema.optional(Schema.String), table: Schema.String, diff --git a/apps/link-preview-worker/package.json b/apps/link-preview-worker/package.json index 6ae893f7a..752c59768 100644 --- a/apps/link-preview-worker/package.json +++ b/apps/link-preview-worker/package.json @@ -12,7 +12,6 @@ "cf-typegen": "wrangler types" }, "dependencies": { - "@effect/platform": "catalog:effect", "effect": "catalog:effect", "metascraper": "^5.49.5", "metascraper-description": "^5.49.5", @@ -25,7 +24,6 @@ }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.8.19", - "@effect/language-service": "catalog:effect", "typescript": "^5.9.3", "vitest": "^4.1.0", "wrangler": "^4.73.0" diff --git a/apps/link-preview-worker/src/api.ts b/apps/link-preview-worker/src/api.ts index 2d61684ab..bcf30fcdb 100644 --- a/apps/link-preview-worker/src/api.ts +++ b/apps/link-preview-worker/src/api.ts @@ -1,4 +1,4 @@ -import { HttpApi, OpenApi } from "@effect/platform" +import { HttpApi, OpenApi } from "effect/unstable/httpapi" import { AppApi, LinkPreviewGroup, TweetGroup } from "./declare" export class LinkPreviewApi extends HttpApi.make("api") diff --git a/apps/link-preview-worker/src/cache.ts b/apps/link-preview-worker/src/cache.ts index 06dac7e92..f9eac6131 100644 --- a/apps/link-preview-worker/src/cache.ts +++ b/apps/link-preview-worker/src/cache.ts @@ -1,4 +1,4 @@ -import { Context, Effect, Layer } from "effect" +import { Effect, Layer, ServiceMap } from "effect" /** * Cache TTL in seconds (1 hour) @@ -9,13 +9,12 @@ const CACHE_TTL = 3600 * KV Cache Service * Provides caching functionality using Cloudflare KV */ -export class KVCache extends Context.Tag("KVCache")< - KVCache, +export class KVCache extends ServiceMap.Service(key: string) => Effect.Effect set: (key: string, value: T) => Effect.Effect } ->() {} +>()("KVCache") {} /** * Create a KV Cache Layer from a KV namespace binding diff --git a/apps/link-preview-worker/src/declare.ts b/apps/link-preview-worker/src/declare.ts index fb28437ae..db61b1767 100644 --- a/apps/link-preview-worker/src/declare.ts +++ b/apps/link-preview-worker/src/declare.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" // Health check API @@ -22,14 +22,12 @@ export class LinkPreviewData extends Schema.Class("LinkPreviewD publisher: Schema.optional(Schema.String), }) {} -export class LinkPreviewError extends Schema.TaggedError("LinkPreviewError")( +export class LinkPreviewError extends Schema.TaggedErrorClass("LinkPreviewError")( "LinkPreviewError", { message: Schema.String, }, - HttpApiSchema.annotations({ - status: 500, - }), + HttpApiSchema.status(500), ) {} export class LinkPreviewGroup extends HttpApiGroup.make("linkPreview") @@ -53,14 +51,12 @@ export class LinkPreviewGroup extends HttpApiGroup.make("linkPreview") .prefix("/link-preview") {} // Tweet Schemas -export class TweetError extends Schema.TaggedError("TweetError")( +export class TweetError extends Schema.TaggedErrorClass("TweetError")( "TweetError", { message: Schema.String, }, - HttpApiSchema.annotations({ - status: 500, - }), + HttpApiSchema.status(500), ) {} export class TweetGroup extends HttpApiGroup.make("tweet") diff --git a/apps/link-preview-worker/src/handle.ts b/apps/link-preview-worker/src/handle.ts index c0b911493..7a287ef50 100644 --- a/apps/link-preview-worker/src/handle.ts +++ b/apps/link-preview-worker/src/handle.ts @@ -1,4 +1,4 @@ -import { HttpApiBuilder } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { Effect } from "effect" import { LinkPreviewApi } from "./api" import { HttpLinkPreviewLive } from "./handlers/link-preview" diff --git a/apps/link-preview-worker/src/handlers/link-preview.ts b/apps/link-preview-worker/src/handlers/link-preview.ts index 4a09b1534..522dedfba 100644 --- a/apps/link-preview-worker/src/handlers/link-preview.ts +++ b/apps/link-preview-worker/src/handlers/link-preview.ts @@ -1,4 +1,5 @@ -import { FetchHttpClient, HttpApiBuilder, HttpClient } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { FetchHttpClient, HttpClient } from "effect/unstable/http" import { Effect } from "effect" import metascraper from "metascraper" import metascraperDescription from "metascraper-description" @@ -29,7 +30,7 @@ function validateImageUrl(url: string): Effect.Effect { return contentType ? contentType.startsWith("image/") : false }), Effect.timeout("1 seconds"), - Effect.catchAll(() => Effect.succeed(false)), + Effect.catch(() => Effect.succeed(false)), Effect.provide(FetchHttpClient.layer), ) } @@ -86,7 +87,7 @@ export const HttpLinkPreviewLive = HttpApiBuilder.group(LinkPreviewApi, "linkPre logo?: { url: string } publisher?: string }>(cacheKey) - .pipe(Effect.catchAll(() => Effect.succeed(null))) + .pipe(Effect.catch(() => Effect.succeed(null))) if (cachedData) { yield* Effect.logDebug(`Cache hit for: ${targetUrl}`) @@ -169,7 +170,7 @@ export const HttpLinkPreviewLive = HttpApiBuilder.group(LinkPreviewApi, "linkPre yield* cache .set(cacheKey, result) .pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.logDebug(`Failed to cache result: ${error.message}`).pipe( Effect.andThen(Effect.succeed(undefined)), ), diff --git a/apps/link-preview-worker/src/handlers/tweet.ts b/apps/link-preview-worker/src/handlers/tweet.ts index 6858b3de6..4961e1ec1 100644 --- a/apps/link-preview-worker/src/handlers/tweet.ts +++ b/apps/link-preview-worker/src/handlers/tweet.ts @@ -1,4 +1,4 @@ -import { HttpApiBuilder } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { Effect } from "effect" import { LinkPreviewApi } from "../api" import { KVCache } from "../cache" @@ -17,7 +17,7 @@ export const HttpTweetLive = HttpApiBuilder.group(LinkPreviewApi, "tweet", (hand // Check cache first const cachedData = yield* cache .get(cacheKey) - .pipe(Effect.catchAll(() => Effect.succeed(null))) + .pipe(Effect.catch(() => Effect.succeed(null))) if (cachedData) { yield* Effect.logDebug(`Cache hit for tweet: ${tweetId}`) @@ -45,7 +45,7 @@ export const HttpTweetLive = HttpApiBuilder.group(LinkPreviewApi, "tweet", (hand // Store in cache (don't fail request if caching fails) yield* cache.set(cacheKey, tweet).pipe( - Effect.catchAll((error) => { + Effect.catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error) return Effect.logDebug(`Failed to cache tweet: ${errorMessage}`).pipe( Effect.andThen(Effect.succeed(undefined)), diff --git a/apps/link-preview-worker/src/index.ts b/apps/link-preview-worker/src/index.ts index 0ad1224c1..aa0be5b05 100644 --- a/apps/link-preview-worker/src/index.ts +++ b/apps/link-preview-worker/src/index.ts @@ -1,4 +1,5 @@ -import { HttpApiBuilder, HttpServer } from "@effect/platform" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpServer } from "effect/unstable/http" import { Layer, Logger, pipe } from "effect" import { LinkPreviewApi } from "./api" import { makeKVCacheLayer } from "./cache" diff --git a/apps/link-preview-worker/src/services/twitter.ts b/apps/link-preview-worker/src/services/twitter.ts index d53916b1a..b45413d96 100644 --- a/apps/link-preview-worker/src/services/twitter.ts +++ b/apps/link-preview-worker/src/services/twitter.ts @@ -1,10 +1,10 @@ -import { FetchHttpClient, HttpClient } from "@effect/platform" -import { Effect, Schema } from "effect" +import { FetchHttpClient, HttpClient } from "effect/unstable/http" +import { ServiceMap, Effect, Schema } from "effect" const SYNDICATION_URL = "https://cdn.syndication.twimg.com" const TWEET_ID_REGEX = /^[0-9]+$/ -export class TwitterApiError extends Schema.TaggedError("TwitterApiError")( +export class TwitterApiError extends Schema.TaggedErrorClass("TwitterApiError")( "TwitterApiError", { message: Schema.String, @@ -72,8 +72,8 @@ function buildTweetUrl(id: string): string { * Twitter API Service * Provides methods to interact with Twitter's syndication API */ -export class TwitterApi extends Effect.Service()("TwitterApi", { - effect: Effect.gen(function* () { +export class TwitterApi extends ServiceMap.Service()("TwitterApi", { + make: Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient return { @@ -109,7 +109,7 @@ export class TwitterApi extends Effect.Service()("TwitterApi", { // Parse JSON response const data: any = yield* response.json.pipe( - Effect.catchAll(() => Effect.succeed(undefined)), + Effect.catch(() => Effect.succeed(undefined)), ) // Handle successful response diff --git a/apps/web/package.json b/apps/web/package.json index 4d06e6dba..f3e0fa45f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,12 +14,9 @@ "dependencies": { "@cloudflare/realtimekit-react": "^1.2.4", "@cloudflare/realtimekit-react-ui": "^1.1.0", - "@effect-atom/atom-react": "catalog:effect", - "@effect/experimental": "catalog:effect", + "@effect/atom-react": "catalog:effect", "@effect/opentelemetry": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-browser": "catalog:effect", - "@effect/rpc": "catalog:effect", "@electric-sql/client": "1.5.12", "@fontsource/inter": "^5.2.8", "@hazel/actors": "workspace:*", @@ -96,7 +93,6 @@ "workbox-window": "^7.4.0" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@react-types/overlays": "^3.9.4", "@react-types/shared": "^3.33.1", "@tailwindcss/postcss": "^4.2.1", diff --git a/apps/web/src/atoms/chat-atoms.ts b/apps/web/src/atoms/chat-atoms.ts index 0e2c2a9bf..5d1e20da5 100644 --- a/apps/web/src/atoms/chat-atoms.ts +++ b/apps/web/src/atoms/chat-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import type { AttachmentId, ChannelId, MessageId } from "@hazel/schema" /** diff --git a/apps/web/src/atoms/chat-query-atoms.ts b/apps/web/src/atoms/chat-query-atoms.ts index e41061a24..6b00d7880 100644 --- a/apps/web/src/atoms/chat-query-atoms.ts +++ b/apps/web/src/atoms/chat-query-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import type { Message, PinnedMessage, User } from "@hazel/domain/models" import type { ChannelId } from "@hazel/schema" import { eq } from "@tanstack/db" diff --git a/apps/web/src/atoms/command-palette-state.ts b/apps/web/src/atoms/command-palette-state.ts index f119e24db..1e82000ef 100644 --- a/apps/web/src/atoms/command-palette-state.ts +++ b/apps/web/src/atoms/command-palette-state.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import type { FilterType, SearchFilter } from "~/lib/search-filter-parser" /** diff --git a/apps/web/src/atoms/custom-emoji-atoms.ts b/apps/web/src/atoms/custom-emoji-atoms.ts index a9705896c..18e3dd0e4 100644 --- a/apps/web/src/atoms/custom-emoji-atoms.ts +++ b/apps/web/src/atoms/custom-emoji-atoms.ts @@ -1,4 +1,4 @@ -import { Atom, Result } from "@effect-atom/atom-react" +import { Atom, Result } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { and, eq, isNull } from "@tanstack/db" import { customEmojiCollection } from "~/db/collections" diff --git a/apps/web/src/atoms/desktop-auth.ts b/apps/web/src/atoms/desktop-auth.ts index 37cf7bd7c..6d11a6991 100644 --- a/apps/web/src/atoms/desktop-auth.ts +++ b/apps/web/src/atoms/desktop-auth.ts @@ -7,7 +7,7 @@ * This module owns atom definitions, init, login, logout, and the scheduler. */ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { Clipboard } from "@effect/platform-browser" import type { OrganizationId } from "@hazel/schema" import { Duration, Effect, Layer, Option, Schema } from "effect" @@ -116,7 +116,7 @@ export const desktopLoginAtom = Atom.fn( return authResult }).pipe( Effect.provide(Layer.mergeAll(TauriAuthLive, TokenStorageLive)), - Effect.catchAll((error) => { + Effect.catch((error) => { console.error("[desktop-auth] Login failed:", error) get.set(desktopAuthStatusAtom, "error") get.set(desktopAuthErrorAtom, { @@ -144,7 +144,7 @@ export const desktopLogoutAtom = Atom.fn( yield* TokenStorage.clearTokens.pipe( Effect.provide(TokenStorageLive), - Effect.catchAll((error) => { + Effect.catch((error) => { console.error("[desktop-auth] Failed to clear tokens:", error) return Effect.void }), @@ -211,7 +211,7 @@ export const desktopLoginFromClipboardAtom = Atom.fn( return tokens }).pipe( Effect.provide(Layer.mergeAll(ClipboardLive, TokenExchangeLive, TokenStorageLive)), - Effect.catchAll((error) => { + Effect.catch((error) => { console.error("[desktop-auth] Clipboard login failed:", error) get.set(desktopAuthStatusAtom, "error") get.set(desktopAuthErrorAtom, { @@ -260,7 +260,7 @@ export const desktopInitAtom = Atom.make((get) => { } }).pipe( Effect.provide(TokenStorageLive), - Effect.catchAll((error) => { + Effect.catch((error) => { console.error("[desktop-auth] Failed to load tokens:", error) get.set(desktopAuthStatusAtom, "error") get.set(desktopAuthErrorAtom, { @@ -335,7 +335,7 @@ export const clearDesktopTokens = (): Promise => { return runtime.runPromise( TokenStorage.clearTokens.pipe( Effect.provide(TokenStorageLive), - Effect.catchAll((error) => { + Effect.catch((error) => { console.error("[desktop-auth] Failed to clear tokens during recovery:", error) return Effect.void }), diff --git a/apps/web/src/atoms/desktop-callback-atoms.ts b/apps/web/src/atoms/desktop-callback-atoms.ts index 9dc5a00ae..f47b118c4 100644 --- a/apps/web/src/atoms/desktop-callback-atoms.ts +++ b/apps/web/src/atoms/desktop-callback-atoms.ts @@ -4,7 +4,7 @@ * @description Effect Atom-based state management for desktop OAuth callback handling */ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { DesktopConnectionError, InvalidDesktopStateError, @@ -215,7 +215,7 @@ const handleCallback = (params: DesktopCallbackParams, get: AtomGetter) => schedule: Schedule.exponential("500 millis"), }), Effect.map(() => ({ success: true as const })), - Effect.catchAll((e) => { + Effect.catch((e) => { const error = new DesktopConnectionError({ message: "Could not connect to Hazel", port, diff --git a/apps/web/src/atoms/emoji-atoms.ts b/apps/web/src/atoms/emoji-atoms.ts index b31b125a9..c44cfa153 100644 --- a/apps/web/src/atoms/emoji-atoms.ts +++ b/apps/web/src/atoms/emoji-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/feature-discovery-atoms.ts b/apps/web/src/atoms/feature-discovery-atoms.ts index 8f599aa3e..75f9403c3 100644 --- a/apps/web/src/atoms/feature-discovery-atoms.ts +++ b/apps/web/src/atoms/feature-discovery-atoms.ts @@ -1,4 +1,4 @@ -import { Atom, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Atom, useAtomSet, useAtomValue } from "@effect/atom-react" import { Schema } from "effect" import { useCallback } from "react" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/hotkey-atoms.ts b/apps/web/src/atoms/hotkey-atoms.ts index 616f69791..c90a4e9de 100644 --- a/apps/web/src/atoms/hotkey-atoms.ts +++ b/apps/web/src/atoms/hotkey-atoms.ts @@ -1,4 +1,4 @@ -import { Atom, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Atom, useAtomSet, useAtomValue } from "@effect/atom-react" import { normalizeHotkey, validateHotkey, type Hotkey } from "@tanstack/react-hotkeys" import { Schema } from "effect" import { useCallback } from "react" diff --git a/apps/web/src/atoms/loading-state-atoms.ts b/apps/web/src/atoms/loading-state-atoms.ts index 288bdf3b6..d57d3b023 100644 --- a/apps/web/src/atoms/loading-state-atoms.ts +++ b/apps/web/src/atoms/loading-state-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" /** * Loading state enum diff --git a/apps/web/src/atoms/message-atoms.ts b/apps/web/src/atoms/message-atoms.ts index e5eecad25..1718aa463 100644 --- a/apps/web/src/atoms/message-atoms.ts +++ b/apps/web/src/atoms/message-atoms.ts @@ -1,4 +1,4 @@ -import { Atom, Result } from "@effect-atom/atom-react" +import { Atom, Result } from "@effect/atom-react" import type { ChannelId, MessageId, UserId } from "@hazel/schema" import { and, count, eq, isNull } from "@tanstack/db" import { diff --git a/apps/web/src/atoms/modal-atoms.ts b/apps/web/src/atoms/modal-atoms.ts index 48fe58ec0..5bca7b0df 100644 --- a/apps/web/src/atoms/modal-atoms.ts +++ b/apps/web/src/atoms/modal-atoms.ts @@ -1,4 +1,4 @@ -import { Atom, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Atom, useAtomSet, useAtomValue } from "@effect/atom-react" import { useCallback } from "react" /** diff --git a/apps/web/src/atoms/notification-sound-atoms.ts b/apps/web/src/atoms/notification-sound-atoms.ts index 1df4d9ef4..d82d6cc93 100644 --- a/apps/web/src/atoms/notification-sound-atoms.ts +++ b/apps/web/src/atoms/notification-sound-atoms.ts @@ -3,7 +3,7 @@ * @description Atoms for notification sound system state management */ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/onboarding-atoms.ts b/apps/web/src/atoms/onboarding-atoms.ts index 28a57c43f..25e9767ea 100644 --- a/apps/web/src/atoms/onboarding-atoms.ts +++ b/apps/web/src/atoms/onboarding-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" // Step identifiers diff --git a/apps/web/src/atoms/organization-atoms.ts b/apps/web/src/atoms/organization-atoms.ts index 62e9ce2e2..3e1397baf 100644 --- a/apps/web/src/atoms/organization-atoms.ts +++ b/apps/web/src/atoms/organization-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { HazelRpcClient } from "~/lib/services/common/rpc-atom-client" diff --git a/apps/web/src/atoms/panel-atoms.ts b/apps/web/src/atoms/panel-atoms.ts index 2682ef20b..33f280a8f 100644 --- a/apps/web/src/atoms/panel-atoms.ts +++ b/apps/web/src/atoms/panel-atoms.ts @@ -1,4 +1,4 @@ -import { Atom, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Atom, useAtomSet, useAtomValue } from "@effect/atom-react" import { Schema } from "effect" import { useCallback } from "react" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/presence-atoms.ts b/apps/web/src/atoms/presence-atoms.ts index 9231fa6e8..741044a3a 100644 --- a/apps/web/src/atoms/presence-atoms.ts +++ b/apps/web/src/atoms/presence-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" /** * Shared "now" signal used to periodically re-render presence UI. diff --git a/apps/web/src/atoms/react-scan-atoms.ts b/apps/web/src/atoms/react-scan-atoms.ts index 65d5b7965..08ba83d04 100644 --- a/apps/web/src/atoms/react-scan-atoms.ts +++ b/apps/web/src/atoms/react-scan-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/recent-channels-atom.ts b/apps/web/src/atoms/recent-channels-atom.ts index 8d9770df5..8b48a32ac 100644 --- a/apps/web/src/atoms/recent-channels-atom.ts +++ b/apps/web/src/atoms/recent-channels-atom.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/search-atoms.ts b/apps/web/src/atoms/search-atoms.ts index fabf79e98..493df807e 100644 --- a/apps/web/src/atoms/search-atoms.ts +++ b/apps/web/src/atoms/search-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/section-collapse-atoms.ts b/apps/web/src/atoms/section-collapse-atoms.ts index d1ecebab3..58d30f0ce 100644 --- a/apps/web/src/atoms/section-collapse-atoms.ts +++ b/apps/web/src/atoms/section-collapse-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import type { ChannelSectionId } from "@hazel/schema" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/sidebar-atoms.ts b/apps/web/src/atoms/sidebar-atoms.ts index e781e06e3..e8b16e7d1 100644 --- a/apps/web/src/atoms/sidebar-atoms.ts +++ b/apps/web/src/atoms/sidebar-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/tauri-update-atoms.ts b/apps/web/src/atoms/tauri-update-atoms.ts index bfe8e5b88..882f4aa8c 100644 --- a/apps/web/src/atoms/tauri-update-atoms.ts +++ b/apps/web/src/atoms/tauri-update-atoms.ts @@ -4,7 +4,7 @@ * @description Effect Atom-based state management for Tauri app updates */ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { getTauriProcess, getTauriUpdater, diff --git a/apps/web/src/atoms/web-auth.ts b/apps/web/src/atoms/web-auth.ts index 4aab6a3b1..be71b4dd9 100644 --- a/apps/web/src/atoms/web-auth.ts +++ b/apps/web/src/atoms/web-auth.ts @@ -7,7 +7,7 @@ * This module owns atom definitions, init, logout, and the scheduler. */ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { Duration, Effect, Option, Schema } from "effect" import { runtime } from "~/lib/services/common/runtime" import { WebTokenStorage } from "~/lib/services/web/token-storage" @@ -36,7 +36,7 @@ export interface WebAuthError { // Errors // ============================================================================ -class JwtDecodeError extends Schema.TaggedError()("JwtDecodeError", { +class JwtDecodeError extends Schema.TaggedErrorClass()("JwtDecodeError", { message: Schema.String, }) {} @@ -116,7 +116,7 @@ export const webLogoutAtom = Atom.fn( const accessTokenOption = yield* tokenStorage.getAccessToken yield* tokenStorage.clearTokens.pipe( - Effect.catchAll((error) => Effect.logError("[web-auth] Failed to clear tokens", error)), + Effect.catch((error) => Effect.logError("[web-auth] Failed to clear tokens", error)), ) get?.set(webTokensAtom, null) @@ -199,7 +199,7 @@ export const webInitAtom = Atom.make((get) => { } }).pipe( Effect.provide(WebTokenStorageLive), - Effect.catchAll((error) => { + Effect.catch((error) => { console.error("[web-auth] Failed to load tokens:", error) get.set(webAuthStatusAtom, "error") get.set(webAuthErrorAtom, { diff --git a/apps/web/src/atoms/web-callback-atoms.ts b/apps/web/src/atoms/web-callback-atoms.ts index cba456068..32a473b9b 100644 --- a/apps/web/src/atoms/web-callback-atoms.ts +++ b/apps/web/src/atoms/web-callback-atoms.ts @@ -4,7 +4,7 @@ * @description Effect Atom-based state management for web OAuth callback handling (JWT flow) */ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { MissingAuthCodeError, OAuthCallbackError, @@ -231,7 +231,7 @@ const handleCallback = (params: WebCallbackParams) => error, }) }), - Effect.catchAll((error) => { + Effect.catch((error) => { console.error("[web-callback] Token exchange failed:", error) return Effect.succeed({ success: false as const, diff --git a/apps/web/src/components/bots/bot-avatar.tsx b/apps/web/src/components/bots/bot-avatar.tsx index 38c6cae2f..07b49eee2 100644 --- a/apps/web/src/components/bots/bot-avatar.tsx +++ b/apps/web/src/components/bots/bot-avatar.tsx @@ -1,4 +1,4 @@ -import { useAtomValue } from "@effect-atom/atom-react" +import { useAtomValue } from "@effect/atom-react" import type { IntegrationConnection } from "@hazel/domain/models" import { Avatar, type AvatarProps } from "~/components/ui/avatar" import { resolveBotAvatarUrl } from "~/lib/bot-avatar" diff --git a/apps/web/src/components/bots/bot-card.tsx b/apps/web/src/components/bots/bot-card.tsx index 902db68f8..84c9259ae 100644 --- a/apps/web/src/components/bots/bot-card.tsx +++ b/apps/web/src/components/bots/bot-card.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { IntegrationConnection } from "@hazel/domain/models" import { useState } from "react" diff --git a/apps/web/src/components/channel-settings/add-github-repo-modal.tsx b/apps/web/src/components/channel-settings/add-github-repo-modal.tsx index 3fa20e9a6..d4c6b310b 100644 --- a/apps/web/src/components/channel-settings/add-github-repo-modal.tsx +++ b/apps/web/src/components/channel-settings/add-github-repo-modal.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import type { GitHubSubscription } from "@hazel/domain/models" import type { ChannelId } from "@hazel/schema" diff --git a/apps/web/src/components/channel-settings/add-rss-feed-modal.tsx b/apps/web/src/components/channel-settings/add-rss-feed-modal.tsx index 749c5db0e..461f3eabf 100644 --- a/apps/web/src/components/channel-settings/add-rss-feed-modal.tsx +++ b/apps/web/src/components/channel-settings/add-rss-feed-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId } from "@hazel/schema" import { useState } from "react" import { createRssSubscriptionMutation } from "~/atoms/rss-subscription-atoms" diff --git a/apps/web/src/components/channel-settings/create-webhook-form.tsx b/apps/web/src/components/channel-settings/create-webhook-form.tsx index 13142e4bd..f4242b5c1 100644 --- a/apps/web/src/components/channel-settings/create-webhook-form.tsx +++ b/apps/web/src/components/channel-settings/create-webhook-form.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId } from "@hazel/schema" import { type } from "arktype" import { useState } from "react" diff --git a/apps/web/src/components/channel-settings/github-integration-card.tsx b/apps/web/src/components/channel-settings/github-integration-card.tsx index 0be990ea2..aaab50ae8 100644 --- a/apps/web/src/components/channel-settings/github-integration-card.tsx +++ b/apps/web/src/components/channel-settings/github-integration-card.tsx @@ -1,4 +1,4 @@ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { ChannelId, OrganizationId } from "@hazel/schema" import { useCallback, useEffect, useRef, useState } from "react" import { diff --git a/apps/web/src/components/channel-settings/github-subscription-card.tsx b/apps/web/src/components/channel-settings/github-subscription-card.tsx index d9cb2f8eb..a08229653 100644 --- a/apps/web/src/components/channel-settings/github-subscription-card.tsx +++ b/apps/web/src/components/channel-settings/github-subscription-card.tsx @@ -1,4 +1,4 @@ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { GitHubSubscriptionId } from "@hazel/schema" import { useState } from "react" import { diff --git a/apps/web/src/components/channel-settings/integration-card.tsx b/apps/web/src/components/channel-settings/integration-card.tsx index fdc039c9c..7b012081a 100644 --- a/apps/web/src/components/channel-settings/integration-card.tsx +++ b/apps/web/src/components/channel-settings/integration-card.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId, ChannelWebhookId } from "@hazel/schema" import { formatDistanceToNow } from "date-fns" import { useState } from "react" diff --git a/apps/web/src/components/channel-settings/openstatus-section.tsx b/apps/web/src/components/channel-settings/openstatus-section.tsx index 185670555..e8d4f0487 100644 --- a/apps/web/src/components/channel-settings/openstatus-section.tsx +++ b/apps/web/src/components/channel-settings/openstatus-section.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId, ChannelWebhookId } from "@hazel/schema" import { formatDistanceToNow } from "date-fns" import { Exit } from "effect" diff --git a/apps/web/src/components/channel-settings/railway-section.tsx b/apps/web/src/components/channel-settings/railway-section.tsx index a5b8ecf12..fc88647ec 100644 --- a/apps/web/src/components/channel-settings/railway-section.tsx +++ b/apps/web/src/components/channel-settings/railway-section.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId, ChannelWebhookId } from "@hazel/schema" import { formatDistanceToNow } from "date-fns" import { Exit } from "effect" diff --git a/apps/web/src/components/channel-settings/rss-integration-card.tsx b/apps/web/src/components/channel-settings/rss-integration-card.tsx index 5aa633e7b..5ce723142 100644 --- a/apps/web/src/components/channel-settings/rss-integration-card.tsx +++ b/apps/web/src/components/channel-settings/rss-integration-card.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId, RssSubscriptionId } from "@hazel/schema" import { useCallback, useEffect, useRef, useState } from "react" import { diff --git a/apps/web/src/components/chat-sync/add-channel-link-modal.tsx b/apps/web/src/components/chat-sync/add-channel-link-modal.tsx index 70f9c5367..367d6bf33 100644 --- a/apps/web/src/components/chat-sync/add-channel-link-modal.tsx +++ b/apps/web/src/components/chat-sync/add-channel-link-modal.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { Channel } from "@hazel/domain/models" import type { ChannelId, ExternalChannelId, OrganizationId, SyncConnectionId } from "@hazel/schema" import { eq, or, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/components/chat-sync/add-connection-modal.tsx b/apps/web/src/components/chat-sync/add-connection-modal.tsx index deb57a399..4436e189b 100644 --- a/apps/web/src/components/chat-sync/add-connection-modal.tsx +++ b/apps/web/src/components/chat-sync/add-connection-modal.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { useNavigate } from "@tanstack/react-router" import { useMemo, useState } from "react" diff --git a/apps/web/src/components/chat/channel-join-banner.tsx b/apps/web/src/components/chat/channel-join-banner.tsx index 877d698b2..c0b0e7842 100644 --- a/apps/web/src/components/chat/channel-join-banner.tsx +++ b/apps/web/src/components/chat/channel-join-banner.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId } from "@hazel/schema" import { UserId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/components/chat/inline-thread-preview.tsx b/apps/web/src/components/chat/inline-thread-preview.tsx index 3252d045b..d4ec96750 100644 --- a/apps/web/src/components/chat/inline-thread-preview.tsx +++ b/apps/web/src/components/chat/inline-thread-preview.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import type { ChannelId, MessageId, UserId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { formatDistanceToNow } from "date-fns" diff --git a/apps/web/src/components/chat/message-list.tsx b/apps/web/src/components/chat/message-list.tsx index e5a61ead1..6c00d8671 100644 --- a/apps/web/src/components/chat/message-list.tsx +++ b/apps/web/src/components/chat/message-list.tsx @@ -1,5 +1,5 @@ import { LegendList, type LegendListRef, type ViewToken } from "@legendapp/list" -import { useAtomValue } from "@effect-atom/atom-react" +import { useAtomValue } from "@effect/atom-react" import type { ChannelId } from "@hazel/schema" import { useLiveInfiniteQuery } from "@tanstack/react-db" import { memo, useCallback, useImperativeHandle, useMemo, useRef, useState } from "react" diff --git a/apps/web/src/components/chat/message-reply-section.tsx b/apps/web/src/components/chat/message-reply-section.tsx index 01673aac6..a540b0849 100644 --- a/apps/web/src/components/chat/message-reply-section.tsx +++ b/apps/web/src/components/chat/message-reply-section.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import type { Message, User } from "@hazel/domain/models" import type { MessageId } from "@hazel/schema" import { messageWithAuthorAtomFamily } from "~/atoms/message-atoms" diff --git a/apps/web/src/components/chat/message.tsx b/apps/web/src/components/chat/message.tsx index 0ddd89a21..1172e2b7a 100644 --- a/apps/web/src/components/chat/message.tsx +++ b/apps/web/src/components/chat/message.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { MessageId, OrganizationId } from "@hazel/schema" import { format } from "date-fns" import { createContext, memo, useCallback, useMemo, useRef, type ReactNode, type RefObject } from "react" diff --git a/apps/web/src/components/chat/reaction-button.tsx b/apps/web/src/components/chat/reaction-button.tsx index 885de5d7d..b7645d4b1 100644 --- a/apps/web/src/components/chat/reaction-button.tsx +++ b/apps/web/src/components/chat/reaction-button.tsx @@ -1,6 +1,6 @@ "use client" -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import type { UserId } from "@hazel/schema" import { memo, useState } from "react" import { userWithPresenceAtomFamily } from "~/atoms/message-atoms" diff --git a/apps/web/src/components/chat/slate-editor/autocomplete/triggers/emoji-trigger.tsx b/apps/web/src/components/chat/slate-editor/autocomplete/triggers/emoji-trigger.tsx index b3bd2a6cc..a724e3f7d 100644 --- a/apps/web/src/components/chat/slate-editor/autocomplete/triggers/emoji-trigger.tsx +++ b/apps/web/src/components/chat/slate-editor/autocomplete/triggers/emoji-trigger.tsx @@ -1,6 +1,6 @@ "use client" -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { useMemo } from "react" import { customEmojisForOrgAtomFamily } from "~/atoms/custom-emoji-atoms" diff --git a/apps/web/src/components/chat/slate-editor/mention-element.tsx b/apps/web/src/components/chat/slate-editor/mention-element.tsx index 97a3cfba6..31eb65fa1 100644 --- a/apps/web/src/components/chat/slate-editor/mention-element.tsx +++ b/apps/web/src/components/chat/slate-editor/mention-element.tsx @@ -1,6 +1,6 @@ "use client" -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import type { UserId } from "@hazel/schema" import { Button as PrimitiveButton } from "react-aria-components" import type { RenderElementProps } from "slate-react" diff --git a/apps/web/src/components/chat/slate-editor/slate-message-editor.tsx b/apps/web/src/components/chat/slate-editor/slate-message-editor.tsx index 5e5b076c8..cda9dc7cd 100644 --- a/apps/web/src/components/chat/slate-editor/slate-message-editor.tsx +++ b/apps/web/src/components/chat/slate-editor/slate-message-editor.tsx @@ -1,6 +1,6 @@ "use client" -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { BotId, ChannelId, OrganizationId } from "@hazel/schema" import { Exit, pipe } from "effect" import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react" diff --git a/apps/web/src/components/chat/slate-editor/slate-message-viewer.tsx b/apps/web/src/components/chat/slate-editor/slate-message-viewer.tsx index 521c21a8a..5daebfc33 100644 --- a/apps/web/src/components/chat/slate-editor/slate-message-viewer.tsx +++ b/apps/web/src/components/chat/slate-editor/slate-message-viewer.tsx @@ -1,6 +1,6 @@ "use client" -import { useAtomValue } from "@effect-atom/atom-react" +import { useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { memo, useCallback, useEffect, useMemo } from "react" import { customEmojiMapAtomFamily } from "~/atoms/custom-emoji-atoms" diff --git a/apps/web/src/components/chat/thread-message-list.tsx b/apps/web/src/components/chat/thread-message-list.tsx index 3ec874266..2ef4b87f0 100644 --- a/apps/web/src/components/chat/thread-message-list.tsx +++ b/apps/web/src/components/chat/thread-message-list.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import type { ChannelId } from "@hazel/schema" import { useCallback, useMemo } from "react" import { createPortal } from "react-dom" diff --git a/apps/web/src/components/chat/thread-panel.tsx b/apps/web/src/components/chat/thread-panel.tsx index b99a81c1d..73455c2d4 100644 --- a/apps/web/src/components/chat/thread-panel.tsx +++ b/apps/web/src/components/chat/thread-panel.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId, MessageId, OrganizationId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { format } from "date-fns" diff --git a/apps/web/src/components/chat/user-profile-popover.tsx b/apps/web/src/components/chat/user-profile-popover.tsx index 6b3ab4940..6139c00e1 100644 --- a/apps/web/src/components/chat/user-profile-popover.tsx +++ b/apps/web/src/components/chat/user-profile-popover.tsx @@ -1,6 +1,6 @@ import { IconChatBubble } from "~/components/icons/icon-chat-bubble" import { IconClock } from "~/components/icons/icon-clock" -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import type { UserId } from "@hazel/schema" import { useNavigate } from "@tanstack/react-router" import { useEffect, useRef, useState } from "react" diff --git a/apps/web/src/components/command-palette/pages/create-channel-view.tsx b/apps/web/src/components/command-palette/pages/create-channel-view.tsx index de8cd9f4d..9e58c82f8 100644 --- a/apps/web/src/components/command-palette/pages/create-channel-view.tsx +++ b/apps/web/src/components/command-palette/pages/create-channel-view.tsx @@ -1,6 +1,6 @@ "use client" -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { useNavigate } from "@tanstack/react-router" import { type } from "arktype" import IconHashtag from "~/components/icons/icon-hashtag" diff --git a/apps/web/src/components/command-palette/pages/home-view.tsx b/apps/web/src/components/command-palette/pages/home-view.tsx index 7fa662382..37a8e1e56 100644 --- a/apps/web/src/components/command-palette/pages/home-view.tsx +++ b/apps/web/src/components/command-palette/pages/home-view.tsx @@ -1,6 +1,6 @@ "use client" -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { and, eq, inArray, or, useLiveQuery } from "@tanstack/react-db" import { useNavigate } from "@tanstack/react-router" import { useCallback, useMemo } from "react" diff --git a/apps/web/src/components/command-palette/pages/join-channel-view.tsx b/apps/web/src/components/command-palette/pages/join-channel-view.tsx index 8cc5e45c7..670d4084a 100644 --- a/apps/web/src/components/command-palette/pages/join-channel-view.tsx +++ b/apps/web/src/components/command-palette/pages/join-channel-view.tsx @@ -1,6 +1,6 @@ "use client" -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId, UserId } from "@hazel/schema" import { eq, inArray, not, or, useLiveQuery } from "@tanstack/react-db" import { useMemo } from "react" diff --git a/apps/web/src/components/command-palette/search-view.tsx b/apps/web/src/components/command-palette/search-view.tsx index 5a1f92fda..369e6cecb 100644 --- a/apps/web/src/components/command-palette/search-view.tsx +++ b/apps/web/src/components/command-palette/search-view.tsx @@ -1,6 +1,6 @@ "use client" -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { UserId } from "@hazel/schema" import { useNavigate } from "@tanstack/react-router" import { formatDistanceToNow } from "date-fns" diff --git a/apps/web/src/components/connect/share-channel-modal.tsx b/apps/web/src/components/connect/share-channel-modal.tsx index 67fb9ba4f..62500f2b4 100644 --- a/apps/web/src/components/connect/share-channel-modal.tsx +++ b/apps/web/src/components/connect/share-channel-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId, OrganizationId } from "@hazel/schema" import { useCallback, useEffect, useRef, useState } from "react" import { createConnectInviteMutation, workspaceSearchMutation } from "~/atoms/connect-share-atoms" diff --git a/apps/web/src/components/embeds/use-embed-theme.ts b/apps/web/src/components/embeds/use-embed-theme.ts index d285bea5f..2b13dfbd8 100644 --- a/apps/web/src/components/embeds/use-embed-theme.ts +++ b/apps/web/src/components/embeds/use-embed-theme.ts @@ -1,4 +1,4 @@ -import { useAtomValue } from "@effect-atom/atom-react" +import { useAtomValue } from "@effect/atom-react" import { getBrandfetchIcon } from "~/lib/integrations/__data" import { resolvedThemeAtom } from "../theme-provider" diff --git a/apps/web/src/components/emoji-picker/custom-emoji-section.tsx b/apps/web/src/components/emoji-picker/custom-emoji-section.tsx index 3b4d0e311..2038b1e0d 100644 --- a/apps/web/src/components/emoji-picker/custom-emoji-section.tsx +++ b/apps/web/src/components/emoji-picker/custom-emoji-section.tsx @@ -1,6 +1,6 @@ "use client" -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { memo } from "react" import { customEmojisForOrgAtomFamily } from "~/atoms/custom-emoji-atoms" diff --git a/apps/web/src/components/gif-picker/use-klipy.ts b/apps/web/src/components/gif-picker/use-klipy.ts index d203744cf..28fd24df7 100644 --- a/apps/web/src/components/gif-picker/use-klipy.ts +++ b/apps/web/src/components/gif-picker/use-klipy.ts @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { KlipyCategory, KlipyGif, KlipySearchResponse } from "@hazel/domain/http" import { Exit } from "effect" import { useCallback, useMemo, useRef, useState } from "react" diff --git a/apps/web/src/components/integrations/add-github-subscription-modal.tsx b/apps/web/src/components/integrations/add-github-subscription-modal.tsx index 37bbacaf3..a62cd8a37 100644 --- a/apps/web/src/components/integrations/add-github-subscription-modal.tsx +++ b/apps/web/src/components/integrations/add-github-subscription-modal.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { Channel, GitHubSubscription } from "@hazel/domain/models" import type { ChannelId, OrganizationId } from "@hazel/schema" import { eq, or, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/components/integrations/add-rss-subscription-modal.tsx b/apps/web/src/components/integrations/add-rss-subscription-modal.tsx index 676ef05d7..a7248b559 100644 --- a/apps/web/src/components/integrations/add-rss-subscription-modal.tsx +++ b/apps/web/src/components/integrations/add-rss-subscription-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { Channel } from "@hazel/domain/models" import type { ChannelId, OrganizationId } from "@hazel/schema" import { eq, or, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/components/integrations/edit-github-subscription-modal.tsx b/apps/web/src/components/integrations/edit-github-subscription-modal.tsx index a56b33f9f..0b5093a96 100644 --- a/apps/web/src/components/integrations/edit-github-subscription-modal.tsx +++ b/apps/web/src/components/integrations/edit-github-subscription-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { GitHubSubscription } from "@hazel/domain/models" import type { GitHubSubscriptionId } from "@hazel/schema" import { useState } from "react" diff --git a/apps/web/src/components/integrations/github-pr-embed.tsx b/apps/web/src/components/integrations/github-pr-embed.tsx index 0c338c971..57a3df762 100644 --- a/apps/web/src/components/integrations/github-pr-embed.tsx +++ b/apps/web/src/components/integrations/github-pr-embed.tsx @@ -1,6 +1,6 @@ "use client" -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { HazelApiClient } from "~/lib/services/common/atom-client" import { cn } from "~/lib/utils" diff --git a/apps/web/src/components/integrations/github-subscriptions-section.tsx b/apps/web/src/components/integrations/github-subscriptions-section.tsx index 7557195c8..3626ff95a 100644 --- a/apps/web/src/components/integrations/github-subscriptions-section.tsx +++ b/apps/web/src/components/integrations/github-subscriptions-section.tsx @@ -1,4 +1,4 @@ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { Channel } from "@hazel/domain/models" import type { GitHubSubscriptionId, OrganizationId } from "@hazel/schema" import { eq, or, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/components/integrations/linear-issue-embed.tsx b/apps/web/src/components/integrations/linear-issue-embed.tsx index bda43014c..eb50ea95a 100644 --- a/apps/web/src/components/integrations/linear-issue-embed.tsx +++ b/apps/web/src/components/integrations/linear-issue-embed.tsx @@ -1,6 +1,6 @@ "use client" -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { HazelApiClient } from "~/lib/services/common/atom-client" import { cn } from "~/lib/utils" diff --git a/apps/web/src/components/integrations/openstatus-integration-content.tsx b/apps/web/src/components/integrations/openstatus-integration-content.tsx index 4fb4ac27c..69b17434b 100644 --- a/apps/web/src/components/integrations/openstatus-integration-content.tsx +++ b/apps/web/src/components/integrations/openstatus-integration-content.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { Channel } from "@hazel/domain/models" import type { ChannelId, OrganizationId } from "@hazel/schema" import { eq, or, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/components/integrations/railway-integration-content.tsx b/apps/web/src/components/integrations/railway-integration-content.tsx index 13dfce3e8..051de40af 100644 --- a/apps/web/src/components/integrations/railway-integration-content.tsx +++ b/apps/web/src/components/integrations/railway-integration-content.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { Channel } from "@hazel/domain/models" import type { ChannelId, OrganizationId } from "@hazel/schema" import { eq, or, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/components/integrations/rss-subscriptions-section.tsx b/apps/web/src/components/integrations/rss-subscriptions-section.tsx index 38d23d816..2a2d44520 100644 --- a/apps/web/src/components/integrations/rss-subscriptions-section.tsx +++ b/apps/web/src/components/integrations/rss-subscriptions-section.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { Channel } from "@hazel/domain/models" import type { OrganizationId, RssSubscriptionId } from "@hazel/schema" import { eq, or, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/components/link-preview.tsx b/apps/web/src/components/link-preview.tsx index 45c7ec402..ddd5b5c63 100644 --- a/apps/web/src/components/link-preview.tsx +++ b/apps/web/src/components/link-preview.tsx @@ -1,6 +1,6 @@ "use client" -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import { useMemo } from "react" import { LinkPreviewClient } from "~/lib/services/common/link-preview-client" diff --git a/apps/web/src/components/modals/create-bot-modal.tsx b/apps/web/src/components/modals/create-bot-modal.tsx index d16609211..9caf426b4 100644 --- a/apps/web/src/components/modals/create-bot-modal.tsx +++ b/apps/web/src/components/modals/create-bot-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ApiScope } from "@hazel/domain/scopes" import { type } from "arktype" import { useState } from "react" diff --git a/apps/web/src/components/modals/create-channel-modal.tsx b/apps/web/src/components/modals/create-channel-modal.tsx index eb998049a..1871049e3 100644 --- a/apps/web/src/components/modals/create-channel-modal.tsx +++ b/apps/web/src/components/modals/create-channel-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { useNavigate } from "@tanstack/react-router" import { type } from "arktype" import { useState } from "react" diff --git a/apps/web/src/components/modals/create-dm-modal.tsx b/apps/web/src/components/modals/create-dm-modal.tsx index c41d392f7..8c5852ec9 100644 --- a/apps/web/src/components/modals/create-dm-modal.tsx +++ b/apps/web/src/components/modals/create-dm-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { User } from "@hazel/domain/models" import type { UserId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/components/modals/create-organization-modal.tsx b/apps/web/src/components/modals/create-organization-modal.tsx index b92fd2218..fb84f8c0d 100644 --- a/apps/web/src/components/modals/create-organization-modal.tsx +++ b/apps/web/src/components/modals/create-organization-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { type } from "arktype" import { useCallback } from "react" import { createOrganizationMutation } from "~/atoms/organization-atoms" diff --git a/apps/web/src/components/modals/create-section-modal.tsx b/apps/web/src/components/modals/create-section-modal.tsx index f709cb44a..9895b0adf 100644 --- a/apps/web/src/components/modals/create-section-modal.tsx +++ b/apps/web/src/components/modals/create-section-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { type } from "arktype" import { Button } from "~/components/ui/button" import { FieldError, Label } from "~/components/ui/field" diff --git a/apps/web/src/components/modals/delete-channel-modal.tsx b/apps/web/src/components/modals/delete-channel-modal.tsx index 995670c36..b9272494d 100644 --- a/apps/web/src/components/modals/delete-channel-modal.tsx +++ b/apps/web/src/components/modals/delete-channel-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId } from "@hazel/schema" import { useMatchRoute, useNavigate, useParams } from "@tanstack/react-router" import { Button } from "~/components/ui/button" diff --git a/apps/web/src/components/modals/delete-workspace-modal.tsx b/apps/web/src/components/modals/delete-workspace-modal.tsx index e1713254c..cee8477bd 100644 --- a/apps/web/src/components/modals/delete-workspace-modal.tsx +++ b/apps/web/src/components/modals/delete-workspace-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { useState } from "react" import { deleteOrganizationMutation } from "~/atoms/organization-atoms" diff --git a/apps/web/src/components/modals/edit-bot-modal.tsx b/apps/web/src/components/modals/edit-bot-modal.tsx index 939eaf1e6..ac023d597 100644 --- a/apps/web/src/components/modals/edit-bot-modal.tsx +++ b/apps/web/src/components/modals/edit-bot-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ApiScope } from "@hazel/domain/scopes" import { type } from "arktype" import { useEffect } from "react" diff --git a/apps/web/src/components/modals/email-invite-modal.tsx b/apps/web/src/components/modals/email-invite-modal.tsx index f31eb73ca..a0b38a467 100644 --- a/apps/web/src/components/modals/email-invite-modal.tsx +++ b/apps/web/src/components/modals/email-invite-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { createInvitationMutation } from "~/atoms/invitation-atoms" import IconClose from "~/components/icons/icon-close" diff --git a/apps/web/src/components/modals/join-channel-modal.tsx b/apps/web/src/components/modals/join-channel-modal.tsx index 84032dd67..2e32acfa3 100644 --- a/apps/web/src/components/modals/join-channel-modal.tsx +++ b/apps/web/src/components/modals/join-channel-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId } from "@hazel/schema" import { UserId } from "@hazel/schema" import { eq, inArray, not, or, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/components/modals/rename-channel-modal.tsx b/apps/web/src/components/modals/rename-channel-modal.tsx index 482ffbf54..2bfadf4d5 100644 --- a/apps/web/src/components/modals/rename-channel-modal.tsx +++ b/apps/web/src/components/modals/rename-channel-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { type } from "arktype" diff --git a/apps/web/src/components/modals/rename-thread-modal.tsx b/apps/web/src/components/modals/rename-thread-modal.tsx index 8849bc5a9..46faedeb2 100644 --- a/apps/web/src/components/modals/rename-thread-modal.tsx +++ b/apps/web/src/components/modals/rename-thread-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { type } from "arktype" diff --git a/apps/web/src/components/modals/request-integration-modal.tsx b/apps/web/src/components/modals/request-integration-modal.tsx index e5c224e6c..e34a4f1a2 100644 --- a/apps/web/src/components/modals/request-integration-modal.tsx +++ b/apps/web/src/components/modals/request-integration-modal.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { type } from "arktype" import { Exit } from "effect" import { useState } from "react" diff --git a/apps/web/src/components/onboarding/invite-team-step.tsx b/apps/web/src/components/onboarding/invite-team-step.tsx index 2ddebe26e..e4828508c 100644 --- a/apps/web/src/components/onboarding/invite-team-step.tsx +++ b/apps/web/src/components/onboarding/invite-team-step.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import IconClose from "~/components/icons/icon-close" import IconPlus from "~/components/icons/icon-plus" diff --git a/apps/web/src/components/onboarding/org-setup-step.tsx b/apps/web/src/components/onboarding/org-setup-step.tsx index deb58ae63..e007de294 100644 --- a/apps/web/src/components/onboarding/org-setup-step.tsx +++ b/apps/web/src/components/onboarding/org-setup-step.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { type } from "arktype" import { Exit } from "effect" import { createOrganizationMutation } from "~/atoms/organization-atoms" diff --git a/apps/web/src/components/onboarding/profile-info-step.tsx b/apps/web/src/components/onboarding/profile-info-step.tsx index 8844e5f92..7e6a3c64a 100644 --- a/apps/web/src/components/onboarding/profile-info-step.tsx +++ b/apps/web/src/components/onboarding/profile-info-step.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { type } from "arktype" import { Exit } from "effect" import { updateUserMutation } from "~/atoms/user-atoms" diff --git a/apps/web/src/components/onboarding/timezone-selection-step.tsx b/apps/web/src/components/onboarding/timezone-selection-step.tsx index 9771ca414..cd19c8d92 100644 --- a/apps/web/src/components/onboarding/timezone-selection-step.tsx +++ b/apps/web/src/components/onboarding/timezone-selection-step.tsx @@ -1,6 +1,6 @@ import IconMagnifier3 from "~/components/icons/icon-magnifier-3" import { IconMapPin } from "~/components/icons/icon-map-pin" -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { useEffect, useMemo, useState } from "react" import { Exit } from "effect" import { updateUserMutation } from "~/atoms/user-atoms" diff --git a/apps/web/src/components/profile/profile-picture-upload.tsx b/apps/web/src/components/profile/profile-picture-upload.tsx index 4a04b09c6..42f74842f 100644 --- a/apps/web/src/components/profile/profile-picture-upload.tsx +++ b/apps/web/src/components/profile/profile-picture-upload.tsx @@ -1,4 +1,4 @@ -import { useAtomRefresh, useAtomSet } from "@effect-atom/atom-react" +import { useAtomRefresh, useAtomSet } from "@effect/atom-react" import { Exit } from "effect" import { useState } from "react" import { Button, type DropItem, DropZone, FileTrigger } from "react-aria-components" diff --git a/apps/web/src/components/sidebar/channel-item.tsx b/apps/web/src/components/sidebar/channel-item.tsx index 61ed10746..8e2992fbd 100644 --- a/apps/web/src/components/sidebar/channel-item.tsx +++ b/apps/web/src/components/sidebar/channel-item.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { Channel, ChannelMember } from "@hazel/db/schema" import type { ChannelSectionId } from "@hazel/schema" import { useNavigate } from "@tanstack/react-router" diff --git a/apps/web/src/components/sidebar/discoverable-channels.tsx b/apps/web/src/components/sidebar/discoverable-channels.tsx index d3962073c..c9156055b 100644 --- a/apps/web/src/components/sidebar/discoverable-channels.tsx +++ b/apps/web/src/components/sidebar/discoverable-channels.tsx @@ -1,6 +1,6 @@ "use client" -import { useAtomValue } from "@effect-atom/atom-react" +import { useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { and, eq, inArray, isNull, not, or, useLiveQuery } from "@tanstack/react-db" import { sectionCollapsedAtomFamily } from "~/atoms/section-collapse-atoms" diff --git a/apps/web/src/components/sidebar/dm-channel-item.tsx b/apps/web/src/components/sidebar/dm-channel-item.tsx index 31204f34a..dd4ecdc28 100644 --- a/apps/web/src/components/sidebar/dm-channel-item.tsx +++ b/apps/web/src/components/sidebar/dm-channel-item.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId, UserId } from "@hazel/schema" import { useRouter } from "@tanstack/react-router" import { memo, useCallback } from "react" diff --git a/apps/web/src/components/sidebar/section-group.tsx b/apps/web/src/components/sidebar/section-group.tsx index f11991408..25bc761b2 100644 --- a/apps/web/src/components/sidebar/section-group.tsx +++ b/apps/web/src/components/sidebar/section-group.tsx @@ -1,6 +1,6 @@ "use client" -import { useAtom, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtom, useAtomSet, useAtomValue } from "@effect/atom-react" import type { ChannelId, ChannelSectionId } from "@hazel/schema" import type { ReactNode } from "react" import { useDragAndDrop } from "react-aria-components" diff --git a/apps/web/src/components/sidebar/thread-item.tsx b/apps/web/src/components/sidebar/thread-item.tsx index cb09b32e5..15df30c0a 100644 --- a/apps/web/src/components/sidebar/thread-item.tsx +++ b/apps/web/src/components/sidebar/thread-item.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { Channel, ChannelMember } from "@hazel/db/schema" import type { ChannelId } from "@hazel/schema" import { Exit } from "effect" diff --git a/apps/web/src/components/tauri-menu-listener.tsx b/apps/web/src/components/tauri-menu-listener.tsx index 513fc2a27..5ca57397a 100644 --- a/apps/web/src/components/tauri-menu-listener.tsx +++ b/apps/web/src/components/tauri-menu-listener.tsx @@ -1,5 +1,5 @@ import { useEffect } from "react" -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { getTauriEvent, type TauriEventApi } from "@hazel/desktop/bridge" import { useNavigate } from "@tanstack/react-router" import { modalAtomFamily } from "~/atoms/modal-atoms" diff --git a/apps/web/src/components/tauri-update-check.tsx b/apps/web/src/components/tauri-update-check.tsx index 462c139b2..13ded3d5c 100644 --- a/apps/web/src/components/tauri-update-check.tsx +++ b/apps/web/src/components/tauri-update-check.tsx @@ -4,7 +4,7 @@ * @description Check for app updates and prompt user to install (no-op in browser) */ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { useEffect, useRef } from "react" import { toast } from "sonner" import { diff --git a/apps/web/src/components/theme-provider.tsx b/apps/web/src/components/theme-provider.tsx index f425d140d..d6e95c1de 100644 --- a/apps/web/src/components/theme-provider.tsx +++ b/apps/web/src/components/theme-provider.tsx @@ -1,4 +1,4 @@ -import { Atom, useAtomMount, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Atom, useAtomMount, useAtomSet, useAtomValue } from "@effect/atom-react" import type { Theme as ThemeModel } from "@hazel/domain/models" import { Schema } from "effect" import { applyBrandColor, applyGrayPalette, applyRadius } from "~/lib/theme/apply" diff --git a/apps/web/src/components/tweet-embed.tsx b/apps/web/src/components/tweet-embed.tsx index 62dc2c790..f139d603c 100644 --- a/apps/web/src/components/tweet-embed.tsx +++ b/apps/web/src/components/tweet-embed.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import type { User } from "@hazel/domain/models" import { useState } from "react" import { type EnrichedTweet, enrichTweet } from "react-tweet" diff --git a/apps/web/src/hooks/use-bot-avatar-upload.tsx b/apps/web/src/hooks/use-bot-avatar-upload.tsx index 6dfcfc30f..8fb116c0a 100644 --- a/apps/web/src/hooks/use-bot-avatar-upload.tsx +++ b/apps/web/src/hooks/use-bot-avatar-upload.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { BotId } from "@hazel/schema" import { Exit } from "effect" import { useCallback } from "react" diff --git a/apps/web/src/hooks/use-channel-member-actions.ts b/apps/web/src/hooks/use-channel-member-actions.ts index 055192dfe..aea8053bc 100644 --- a/apps/web/src/hooks/use-channel-member-actions.ts +++ b/apps/web/src/hooks/use-channel-member-actions.ts @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelMemberId } from "@hazel/schema" import { deleteChannelMemberMutation } from "~/atoms/channel-member-atoms" import { updateChannelMemberAction } from "~/db/actions" diff --git a/apps/web/src/hooks/use-command-palette.ts b/apps/web/src/hooks/use-command-palette.ts index b68d0ce4d..4509534d6 100644 --- a/apps/web/src/hooks/use-command-palette.ts +++ b/apps/web/src/hooks/use-command-palette.ts @@ -1,4 +1,4 @@ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { useCallback } from "react" import { type CommandPalettePageType, diff --git a/apps/web/src/hooks/use-emoji-stats.tsx b/apps/web/src/hooks/use-emoji-stats.tsx index 2af999a61..fb887ae63 100644 --- a/apps/web/src/hooks/use-emoji-stats.tsx +++ b/apps/web/src/hooks/use-emoji-stats.tsx @@ -1,4 +1,4 @@ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { useCallback } from "react" import { type EmojiUsage, emojiUsageAtom, topEmojisAtom } from "~/atoms/emoji-atoms" diff --git a/apps/web/src/hooks/use-file-upload.tsx b/apps/web/src/hooks/use-file-upload.tsx index 6307a920a..df8f97651 100644 --- a/apps/web/src/hooks/use-file-upload.tsx +++ b/apps/web/src/hooks/use-file-upload.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { AttachmentId, ChannelId, OrganizationId } from "@hazel/schema" import { Exit } from "effect" import { useCallback, useRef, useState } from "react" diff --git a/apps/web/src/hooks/use-loading-state.ts b/apps/web/src/hooks/use-loading-state.ts index 28295de58..75a654fdd 100644 --- a/apps/web/src/hooks/use-loading-state.ts +++ b/apps/web/src/hooks/use-loading-state.ts @@ -1,4 +1,4 @@ -import { useAtomValue } from "@effect-atom/atom-react" +import { useAtomValue } from "@effect/atom-react" import { useCallback, useMemo } from "react" import { canLoadBottomAtomFamily, diff --git a/apps/web/src/hooks/use-notification-settings.ts b/apps/web/src/hooks/use-notification-settings.ts index 56014c7b4..76e1c10ac 100644 --- a/apps/web/src/hooks/use-notification-settings.ts +++ b/apps/web/src/hooks/use-notification-settings.ts @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { type User } from "@hazel/domain/models" import type { UserId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/hooks/use-notification-sound.tsx b/apps/web/src/hooks/use-notification-sound.tsx index 63e05bcad..a6bdcc205 100644 --- a/apps/web/src/hooks/use-notification-sound.tsx +++ b/apps/web/src/hooks/use-notification-sound.tsx @@ -1,4 +1,4 @@ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { useCallback } from "react" import { type NotificationSoundSettings, diff --git a/apps/web/src/hooks/use-onboarding.ts b/apps/web/src/hooks/use-onboarding.ts index 509874e19..09b832f94 100644 --- a/apps/web/src/hooks/use-onboarding.ts +++ b/apps/web/src/hooks/use-onboarding.ts @@ -1,4 +1,4 @@ -import { useAtom, useAtomSet } from "@effect-atom/atom-react" +import { useAtom, useAtomSet } from "@effect/atom-react" import type { OrganizationId, OrganizationMemberId } from "@hazel/schema" import { Exit } from "effect" import { usePostHog } from "posthog-js/react" diff --git a/apps/web/src/hooks/use-organization-avatar-upload.tsx b/apps/web/src/hooks/use-organization-avatar-upload.tsx index e27e208ae..cce3f6c9c 100644 --- a/apps/web/src/hooks/use-organization-avatar-upload.tsx +++ b/apps/web/src/hooks/use-organization-avatar-upload.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { Exit } from "effect" import { useCallback } from "react" diff --git a/apps/web/src/hooks/use-presence.ts b/apps/web/src/hooks/use-presence.ts index 85804378e..e214006b5 100644 --- a/apps/web/src/hooks/use-presence.ts +++ b/apps/web/src/hooks/use-presence.ts @@ -1,4 +1,4 @@ -import { Atom, Result, useAtomMount, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Atom, Result, useAtomMount, useAtomSet, useAtomValue } from "@effect/atom-react" import type { ChannelId, UserId } from "@hazel/schema" import { eq } from "@tanstack/db" import { DateTime, Duration, Effect } from "effect" diff --git a/apps/web/src/hooks/use-profile-picture-upload.tsx b/apps/web/src/hooks/use-profile-picture-upload.tsx index 74aab1323..2902889ab 100644 --- a/apps/web/src/hooks/use-profile-picture-upload.tsx +++ b/apps/web/src/hooks/use-profile-picture-upload.tsx @@ -1,4 +1,4 @@ -import { useAtomRefresh, useAtomSet } from "@effect-atom/atom-react" +import { useAtomRefresh, useAtomSet } from "@effect/atom-react" import type { UserId } from "@hazel/schema" import { Exit } from "effect" import { useCallback } from "react" diff --git a/apps/web/src/hooks/use-theme-settings.ts b/apps/web/src/hooks/use-theme-settings.ts index c76e3ba26..c7003e163 100644 --- a/apps/web/src/hooks/use-theme-settings.ts +++ b/apps/web/src/hooks/use-theme-settings.ts @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { type Theme } from "@hazel/domain/models" import type { UserId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/hooks/use-typing.ts b/apps/web/src/hooks/use-typing.ts index f76dec61d..2e55a32cd 100644 --- a/apps/web/src/hooks/use-typing.ts +++ b/apps/web/src/hooks/use-typing.ts @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId, ChannelMemberId, TypingIndicatorId } from "@hazel/schema" import { Exit } from "effect" import { useCallback, useEffect, useRef, useState } from "react" diff --git a/apps/web/src/hooks/use-upload.ts b/apps/web/src/hooks/use-upload.ts index ec470644d..6e3fe68cc 100644 --- a/apps/web/src/hooks/use-upload.ts +++ b/apps/web/src/hooks/use-upload.ts @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { AttachmentUploadRequest, BotAvatarUploadRequest, diff --git a/apps/web/src/hooks/use-visible-message-notification-cleaner.ts b/apps/web/src/hooks/use-visible-message-notification-cleaner.ts index b051dba3a..8ce3bd8f9 100644 --- a/apps/web/src/hooks/use-visible-message-notification-cleaner.ts +++ b/apps/web/src/hooks/use-visible-message-notification-cleaner.ts @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId, MessageId } from "@hazel/schema" import { and, eq, useLiveQuery } from "@tanstack/react-db" import { useCallback, useEffect, useMemo, useRef } from "react" diff --git a/apps/web/src/lib/auth-fetch.ts b/apps/web/src/lib/auth-fetch.ts index 32803323e..93379980c 100644 --- a/apps/web/src/lib/auth-fetch.ts +++ b/apps/web/src/lib/auth-fetch.ts @@ -27,7 +27,7 @@ const clearTokens = async (): Promise => { : WebTokenStorage.clearTokens.pipe(Effect.provide(WebTokenStorageLive)) return runtime.runPromise( effect.pipe( - Effect.catchAll(() => Effect.void), + Effect.catch(() => Effect.void), Effect.withSpan("clearTokens"), ), ) diff --git a/apps/web/src/lib/auth-token.ts b/apps/web/src/lib/auth-token.ts index c31556f2e..83f985757 100644 --- a/apps/web/src/lib/auth-token.ts +++ b/apps/web/src/lib/auth-token.ts @@ -117,7 +117,7 @@ const storeTokens = Effect.fn("storeTokens")(function* ( * Returns null if not authenticated. */ const getAccessTokenEffect: Effect.Effect = readAccessToken().pipe( - Effect.catchAll(() => Effect.succeed(null)), + Effect.catch(() => Effect.succeed(null)), Effect.withSpan("getAccessToken"), ) @@ -133,7 +133,7 @@ const waitForRefreshEffect: Effect.Effect = Effect.gen(function* () { } return true }).pipe( - Effect.catchAll(() => Effect.succeed(true)), + Effect.catch(() => Effect.succeed(true)), Effect.withSpan("waitForRefresh"), ) @@ -157,7 +157,7 @@ const forceRefreshEffect: Effect.Effect = Effect.gen(function* () { // Get refresh token to check if we can refresh const refreshTokenOpt = yield* readRefreshToken().pipe( - Effect.catchAll(() => Effect.succeed(Option.none())), + Effect.catch(() => Effect.succeed(Option.none())), ) if (Option.isNone(refreshTokenOpt)) { @@ -179,7 +179,7 @@ const forceRefreshEffect: Effect.Effect = Effect.gen(function* () { const refreshResult = yield* tokenExchange.refreshToken(refreshTokenOpt.value).pipe( Effect.map((tokens) => ({ success: true as const, tokens })), - Effect.catchAll((error) => Effect.succeed({ success: false as const, error })), + Effect.catch((error) => Effect.succeed({ success: false as const, error })), ) if (refreshResult.success) { @@ -247,7 +247,7 @@ const forceRefreshEffect: Effect.Effect = Effect.gen(function* () { yield* attemptRefresh(1).pipe( Effect.provide(tokenExchangeLive), Effect.tap((result) => Ref.set(resultRef, result)), - Effect.catchAll((error) => { + Effect.catch((error) => { console.error(`[auth-token:${tag}] Unexpected error during refresh:`, error) return Effect.void }), @@ -263,7 +263,7 @@ const forceRefreshEffect: Effect.Effect = Effect.gen(function* () { return yield* Ref.get(resultRef) }).pipe( - Effect.catchAll(() => Effect.succeed(false)), + Effect.catch(() => Effect.succeed(false)), Effect.withSpan("forceRefresh"), ) diff --git a/apps/web/src/lib/auth.tsx b/apps/web/src/lib/auth.tsx index a564537a5..30511e28a 100644 --- a/apps/web/src/lib/auth.tsx +++ b/apps/web/src/lib/auth.tsx @@ -1,4 +1,4 @@ -import { Atom, Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Atom, Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { desktopInitAtom, diff --git a/apps/web/src/lib/electric-fetch.ts b/apps/web/src/lib/electric-fetch.ts index 5110f74fc..5860cfe64 100644 --- a/apps/web/src/lib/electric-fetch.ts +++ b/apps/web/src/lib/electric-fetch.ts @@ -76,7 +76,7 @@ export const electricFetchClient = async ( while: (error) => error instanceof Response && shouldRetry(error), }), // If all retries exhausted, return the last failed response - Effect.catchAll((error) => (error instanceof Response ? Effect.succeed(error) : Effect.fail(error))), + Effect.catch((error) => (error instanceof Response ? Effect.succeed(error) : Effect.fail(error))), ) return runtime.runPromise(withRetry) diff --git a/apps/web/src/lib/platform-storage/platform-runtime.ts b/apps/web/src/lib/platform-storage/platform-runtime.ts index dc38e48c0..54c352c01 100644 --- a/apps/web/src/lib/platform-storage/platform-runtime.ts +++ b/apps/web/src/lib/platform-storage/platform-runtime.ts @@ -18,7 +18,7 @@ * // Then use platformStorageRuntime in Atom.kvs */ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { layer } from "./platform-key-value-store" export const platformStorageRuntime = Atom.runtime(layer) diff --git a/apps/web/src/lib/registry.ts b/apps/web/src/lib/registry.ts index 0f0eb350b..c85eaf622 100644 --- a/apps/web/src/lib/registry.ts +++ b/apps/web/src/lib/registry.ts @@ -1,4 +1,4 @@ -import { Atom, Registry, scheduleTask } from "@effect-atom/atom-react" +import { Atom, Registry, scheduleTask } from "@effect/atom-react" import { runtimeLayer } from "./services/common/runtime" export const appRegistry = Registry.make({ scheduleTask }) diff --git a/apps/web/src/lib/rpc-auth-middleware.ts b/apps/web/src/lib/rpc-auth-middleware.ts index 99a4b8d16..8f2ea68a1 100644 --- a/apps/web/src/lib/rpc-auth-middleware.ts +++ b/apps/web/src/lib/rpc-auth-middleware.ts @@ -4,8 +4,8 @@ * @description Client-side auth middleware that adds Bearer token from storage (Tauri store or localStorage) */ -import { Headers } from "@effect/platform" -import { RpcMiddleware } from "@effect/rpc" +import { Headers } from "effect/unstable/http" +import { RpcMiddleware } from "effect/unstable/rpc" import { AuthMiddleware } from "@hazel/domain/rpc" import { Effect } from "effect" import { waitForRefreshEffect, getAccessTokenEffect } from "~/lib/auth-token" diff --git a/apps/web/src/lib/services/common/api-client.ts b/apps/web/src/lib/services/common/api-client.ts index 88bf00884..f3f38efc9 100644 --- a/apps/web/src/lib/services/common/api-client.ts +++ b/apps/web/src/lib/services/common/api-client.ts @@ -8,7 +8,7 @@ import * as FetchHttpClient from "@effect/platform/FetchHttpClient" import * as HttpApiClient from "@effect/platform/HttpApiClient" import * as HttpClient from "@effect/platform/HttpClient" import { HazelApi } from "@hazel/domain/http" -import { Layer } from "effect" +import { ServiceMap, Layer } from "effect" import * as Effect from "effect/Effect" import { authenticatedFetch } from "../../auth-fetch" @@ -16,10 +16,9 @@ export const CustomFetchLive = FetchHttpClient.layer.pipe( Layer.provideMerge(Layer.succeed(FetchHttpClient.Fetch, authenticatedFetch)), ) -export class ApiClient extends Effect.Service()("ApiClient", { - accessors: true, +export class ApiClient extends ServiceMap.Service()("ApiClient", { dependencies: [CustomFetchLive], - effect: Effect.gen(function* () { + make: Effect.gen(function* () { return yield* HttpApiClient.make(HazelApi, { baseUrl: import.meta.env.VITE_BACKEND_URL, transformClient: (client) => diff --git a/apps/web/src/lib/services/common/atom-client.ts b/apps/web/src/lib/services/common/atom-client.ts index 19c97bbc8..00caa208e 100644 --- a/apps/web/src/lib/services/common/atom-client.ts +++ b/apps/web/src/lib/services/common/atom-client.ts @@ -1,4 +1,4 @@ -import { AtomHttpApi } from "@effect-atom/atom-react" +import { AtomHttpApi } from "@effect/atom-react" import { HazelApi } from "@hazel/domain/http" import { CustomFetchLive } from "./api-client" diff --git a/apps/web/src/lib/services/common/link-preview-client.ts b/apps/web/src/lib/services/common/link-preview-client.ts index b8187bbd9..0dc9051d7 100644 --- a/apps/web/src/lib/services/common/link-preview-client.ts +++ b/apps/web/src/lib/services/common/link-preview-client.ts @@ -1,5 +1,5 @@ -import { FetchHttpClient } from "@effect/platform" -import { AtomHttpApi } from "@effect-atom/atom-react" +import { FetchHttpClient } from "effect/unstable/http" +import { AtomHttpApi } from "@effect/atom-react" import { LinkPreviewApi } from "@hazel/link-preview-worker" diff --git a/apps/web/src/lib/services/common/network-mode.ts b/apps/web/src/lib/services/common/network-mode.ts index 6d3ab1cc1..3141e5f58 100644 --- a/apps/web/src/lib/services/common/network-mode.ts +++ b/apps/web/src/lib/services/common/network-mode.ts @@ -3,8 +3,8 @@ import * as Effect from "effect/Effect" import * as Stream from "effect/Stream" import * as SubscriptionRef from "effect/SubscriptionRef" -export class NetworkMonitor extends Effect.Service()("NetworkMonitor", { - scoped: Effect.gen(function* () { +export class NetworkMonitor extends ServiceMap.Service()("NetworkMonitor", { + make: Effect.gen(function* () { const latch = yield* Effect.makeLatch(true) const ref = yield* SubscriptionRef.make(window.navigator.onLine) @@ -25,5 +25,4 @@ export class NetworkMonitor extends Effect.Service()("NetworkMon return { latch, ref } }), - accessors: true, }) {} diff --git a/apps/web/src/lib/services/common/rpc-atom-client.ts b/apps/web/src/lib/services/common/rpc-atom-client.ts index d2b06bd46..2561837fe 100644 --- a/apps/web/src/lib/services/common/rpc-atom-client.ts +++ b/apps/web/src/lib/services/common/rpc-atom-client.ts @@ -1,7 +1,7 @@ -import { Reactivity } from "@effect/experimental" -import { FetchHttpClient } from "@effect/platform" -import { RpcClient as RpcClientBuilder, RpcSerialization } from "@effect/rpc" -import { AtomRpc } from "@effect-atom/atom-react" +import { Reactivity } from "effect/unstable/reactivity" +import { FetchHttpClient } from "effect/unstable/http" +import { RpcClient as RpcClientBuilder, RpcSerialization } from "effect/unstable/rpc" +import { AtomRpc } from "@effect/atom-react" import { AuthMiddlewareClientLive } from "~/lib/rpc-auth-middleware" import { AttachmentRpcs, @@ -82,4 +82,4 @@ export class HazelRpcClient extends AtomRpc.Tag()("HazelRpcClien protocol: AtomRpcProtocolLive, }) {} -export type { RpcClientError } from "@effect/rpc" +export type { RpcClientError } from "effect/unstable/rpc" diff --git a/apps/web/src/lib/services/common/runtime.ts b/apps/web/src/lib/services/common/runtime.ts index fbe2c43e8..fba8f719c 100644 --- a/apps/web/src/lib/services/common/runtime.ts +++ b/apps/web/src/lib/services/common/runtime.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect-atom/atom-react" +import { Atom } from "@effect/atom-react" import { Layer, ManagedRuntime } from "effect" import { ApiClient } from "./api-client" import { HazelRpcClient } from "./rpc-atom-client" diff --git a/apps/web/src/lib/services/desktop/tauri-auth.ts b/apps/web/src/lib/services/desktop/tauri-auth.ts index 0d386d489..1ebb037f0 100644 --- a/apps/web/src/lib/services/desktop/tauri-auth.ts +++ b/apps/web/src/lib/services/desktop/tauri-auth.ts @@ -22,7 +22,7 @@ import { TauriCommandError, TauriNotAvailableError, } from "@hazel/domain/errors" -import { Deferred, Duration, Effect, FiberId } from "effect" +import { ServiceMap, Deferred, Duration, Effect, FiberId } from "effect" import { TokenExchange } from "./token-exchange" import { TokenStorage } from "./token-storage" @@ -82,10 +82,9 @@ const getTauriEvent = Effect.gen(function* () { return event }) -export class TauriAuth extends Effect.Service()("TauriAuth", { - accessors: true, +export class TauriAuth extends ServiceMap.Service()("TauriAuth", { dependencies: [TokenStorage.Default, TokenExchange.Default], - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const tokenStorage = yield* TokenStorage const tokenExchange = yield* TokenExchange diff --git a/apps/web/src/lib/services/desktop/token-exchange.ts b/apps/web/src/lib/services/desktop/token-exchange.ts index 264e4b296..9d26aa64b 100644 --- a/apps/web/src/lib/services/desktop/token-exchange.ts +++ b/apps/web/src/lib/services/desktop/token-exchange.ts @@ -4,17 +4,16 @@ * @description HTTP client for token exchange using Effect HttpClient with Schema validation */ -import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "@effect/platform" +import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http" import { OAuthCodeExpiredError, TokenDecodeError, TokenExchangeError } from "@hazel/domain/errors" import { RefreshTokenResponse, TokenResponse } from "@hazel/domain/http" -import { Duration, Effect, Schema } from "effect" +import { ServiceMap, Duration, Effect, Schema } from "effect" const DEFAULT_TIMEOUT = Duration.seconds(60) -export class TokenExchange extends Effect.Service()("TokenExchange", { - accessors: true, +export class TokenExchange extends ServiceMap.Service()("TokenExchange", { dependencies: [FetchHttpClient.layer], - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient const backendUrl = import.meta.env.VITE_BACKEND_URL diff --git a/apps/web/src/lib/services/desktop/token-storage.ts b/apps/web/src/lib/services/desktop/token-storage.ts index 52f7888a6..94ce8b001 100644 --- a/apps/web/src/lib/services/desktop/token-storage.ts +++ b/apps/web/src/lib/services/desktop/token-storage.ts @@ -6,7 +6,7 @@ import { getTauriStore, type TauriStoreApi } from "@hazel/desktop/bridge" import { TokenNotFoundError, TokenStoreError, TauriNotAvailableError } from "@hazel/domain/errors" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" type StoreInstance = Awaited> @@ -48,9 +48,8 @@ const getStore = Effect.gen(function* () { }) }) -export class TokenStorage extends Effect.Service()("TokenStorage", { - accessors: true, - effect: Effect.gen(function* () { +export class TokenStorage extends ServiceMap.Service()("TokenStorage", { + make: Effect.gen(function* () { return { /** * Store all auth tokens in Tauri store diff --git a/apps/web/src/lib/services/web/token-storage.ts b/apps/web/src/lib/services/web/token-storage.ts index 719ab8f4b..786109cfe 100644 --- a/apps/web/src/lib/services/web/token-storage.ts +++ b/apps/web/src/lib/services/web/token-storage.ts @@ -5,7 +5,7 @@ */ import { TokenNotFoundError, TokenStoreError } from "@hazel/domain/errors" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" const STORAGE_PREFIX = "hazel_auth_" const ACCESS_TOKEN_KEY = `${STORAGE_PREFIX}access_token` @@ -28,9 +28,8 @@ const checkStorage = Effect.gen(function* () { return window.localStorage }) -export class WebTokenStorage extends Effect.Service()("WebTokenStorage", { - accessors: true, - effect: Effect.gen(function* () { +export class WebTokenStorage extends ServiceMap.Service()("WebTokenStorage", { + make: Effect.gen(function* () { return { /** * Store all auth tokens in localStorage diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 9825518a0..b4b028a46 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -18,7 +18,7 @@ import "./styles/styles.css" // Initialize app registry and mount runtimes // Note: RPC devtools are now integrated via Effect layers in rpc-atom-client.ts -import { RegistryContext } from "@effect-atom/atom-react" +import { RegistryContext } from "@effect/atom-react" import { appRegistry } from "./lib/registry.ts" // Initialize Tauri-specific features (no-op in browser) diff --git a/apps/web/src/providers/chat-provider.tsx b/apps/web/src/providers/chat-provider.tsx index 01886c110..0c08cb234 100644 --- a/apps/web/src/providers/chat-provider.tsx +++ b/apps/web/src/providers/chat-provider.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { Channel } from "@hazel/domain/models" import { type AttachmentId, diff --git a/apps/web/src/providers/notification-sound-provider.tsx b/apps/web/src/providers/notification-sound-provider.tsx index 781488127..74a3120cd 100644 --- a/apps/web/src/providers/notification-sound-provider.tsx +++ b/apps/web/src/providers/notification-sound-provider.tsx @@ -1,5 +1,5 @@ import type { ChannelId, MessageId } from "@hazel/schema" -import { useAtomValue } from "@effect-atom/atom-react" +import { useAtomValue } from "@effect/atom-react" import { eq, useLiveQuery } from "@tanstack/react-db" import { type ReactNode, useEffect, useMemo, useRef } from "react" import { diff --git a/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/connect.tsx b/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/connect.tsx index e7f5e0272..58c5810fc 100644 --- a/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/connect.tsx +++ b/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/connect.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { ChannelId, ConnectConversationId, ConnectInviteId, OrganizationId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { Option } from "effect" diff --git a/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/integrations.tsx b/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/integrations.tsx index 6a2640e0f..8c0b92292 100644 --- a/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/integrations.tsx +++ b/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/integrations.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId, ChannelWebhookId, OrganizationId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { createFileRoute } from "@tanstack/react-router" diff --git a/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/overview.tsx b/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/overview.tsx index a6daac594..a3bd3d0cb 100644 --- a/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/overview.tsx +++ b/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/overview.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelIcon as ChannelIconType, ChannelId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { createFileRoute } from "@tanstack/react-router" diff --git a/apps/web/src/routes/_app/$orgSlug/chat/$id.tsx b/apps/web/src/routes/_app/$orgSlug/chat/$id.tsx index 919b7f72e..0d5e8fba9 100644 --- a/apps/web/src/routes/_app/$orgSlug/chat/$id.tsx +++ b/apps/web/src/routes/_app/$orgSlug/chat/$id.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { ChannelId } from "@hazel/schema" import { createLiveQueryCollection, eq } from "@tanstack/db" import { createFileRoute, Outlet } from "@tanstack/react-router" diff --git a/apps/web/src/routes/_app/$orgSlug/index.tsx b/apps/web/src/routes/_app/$orgSlug/index.tsx index 5133a4485..64a52c491 100644 --- a/apps/web/src/routes/_app/$orgSlug/index.tsx +++ b/apps/web/src/routes/_app/$orgSlug/index.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { UserId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { createFileRoute, useNavigate, useParams } from "@tanstack/react-router" diff --git a/apps/web/src/routes/_app/$orgSlug/my-settings/desktop.tsx b/apps/web/src/routes/_app/$orgSlug/my-settings/desktop.tsx index 7c89f7fb9..498b50cef 100644 --- a/apps/web/src/routes/_app/$orgSlug/my-settings/desktop.tsx +++ b/apps/web/src/routes/_app/$orgSlug/my-settings/desktop.tsx @@ -4,7 +4,7 @@ * @description User settings specific to the desktop application (autostart, etc.) */ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { createFileRoute } from "@tanstack/react-router" import { useEffect, useState } from "react" import { diff --git a/apps/web/src/routes/_app/$orgSlug/my-settings/linked-accounts.tsx b/apps/web/src/routes/_app/$orgSlug/my-settings/linked-accounts.tsx index 70073df70..e3dcb5bcb 100644 --- a/apps/web/src/routes/_app/$orgSlug/my-settings/linked-accounts.tsx +++ b/apps/web/src/routes/_app/$orgSlug/my-settings/linked-accounts.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { UserId } from "@hazel/schema" import { createFileRoute, useNavigate } from "@tanstack/react-router" import { Exit } from "effect" diff --git a/apps/web/src/routes/_app/$orgSlug/my-settings/profile.tsx b/apps/web/src/routes/_app/$orgSlug/my-settings/profile.tsx index 7932aee16..769d9cb35 100644 --- a/apps/web/src/routes/_app/$orgSlug/my-settings/profile.tsx +++ b/apps/web/src/routes/_app/$orgSlug/my-settings/profile.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { UserId } from "@hazel/schema" import { createFileRoute } from "@tanstack/react-router" import { type } from "arktype" diff --git a/apps/web/src/routes/_app/$orgSlug/profile/$userId.tsx b/apps/web/src/routes/_app/$orgSlug/profile/$userId.tsx index a5705618d..010ea1be1 100644 --- a/apps/web/src/routes/_app/$orgSlug/profile/$userId.tsx +++ b/apps/web/src/routes/_app/$orgSlug/profile/$userId.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomValue } from "@effect/atom-react" import type { UserId } from "@hazel/schema" import { createFileRoute, Link } from "@tanstack/react-router" import { userWithPresenceAtomFamily } from "~/atoms/message-atoms" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/authentication.tsx b/apps/web/src/routes/_app/$orgSlug/settings/authentication.tsx index bde41928b..8608c7a33 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/authentication.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/authentication.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import { createFileRoute } from "@tanstack/react-router" import { useState } from "react" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/$connectionId.tsx b/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/$connectionId.tsx index 96f5246f1..b5cb0aa9d 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/$connectionId.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/$connectionId.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { SyncChannelLinkId, SyncConnectionId } from "@hazel/schema" import { createFileRoute, useNavigate } from "@tanstack/react-router" import { eq, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/index.tsx b/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/index.tsx index 4ae17883f..4f67cdd83 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/index.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/index.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { SyncConnectionId } from "@hazel/schema" import { createFileRoute, useNavigate } from "@tanstack/react-router" import { Option } from "effect" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/connect-invites.tsx b/apps/web/src/routes/_app/$orgSlug/settings/connect-invites.tsx index 438fb525f..90cf5166f 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/connect-invites.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/connect-invites.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { ConnectInviteId, OrganizationId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { Option } from "effect" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/custom-emojis.tsx b/apps/web/src/routes/_app/$orgSlug/settings/custom-emojis.tsx index 9094549af..1fac39189 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/custom-emojis.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/custom-emojis.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { eq, isNull, useLiveQuery } from "@tanstack/react-db" import { createFileRoute } from "@tanstack/react-router" import { formatDistanceToNow } from "date-fns" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/debug.tsx b/apps/web/src/routes/_app/$orgSlug/settings/debug.tsx index 545b026e7..e834907b4 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/debug.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/debug.tsx @@ -1,4 +1,4 @@ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { type Notification } from "@hazel/domain/models" import { IconWarning } from "~/components/icons/icon-warning" import { createFileRoute, redirect } from "@tanstack/react-router" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/index.tsx b/apps/web/src/routes/_app/$orgSlug/settings/index.tsx index 44990ea57..82f764ece 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/index.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/index.tsx @@ -1,4 +1,4 @@ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { createFileRoute, useNavigate, useParams } from "@tanstack/react-router" import { useRef, useState } from "react" import { toast } from "sonner" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx b/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx index d9c75f771..319288d58 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import type { IntegrationConnection } from "@hazel/domain/models" import { createFileRoute, notFound, useNavigate } from "@tanstack/react-router" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/integrations/index.tsx b/apps/web/src/routes/_app/$orgSlug/settings/integrations/index.tsx index 96638611a..390e85fa7 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/integrations/index.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/integrations/index.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { IntegrationConnection } from "@hazel/domain/models" import { createFileRoute, useNavigate } from "@tanstack/react-router" import { useEffect, useMemo, useState } from "react" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/integrations/installed.tsx b/apps/web/src/routes/_app/$orgSlug/settings/integrations/installed.tsx index 2f1ffb102..b0d436dfc 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/integrations/installed.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/integrations/installed.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { BotId } from "@hazel/schema" import { createFileRoute, useNavigate } from "@tanstack/react-router" import { useCallback, useState } from "react" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/integrations/marketplace.tsx b/apps/web/src/routes/_app/$orgSlug/settings/integrations/marketplace.tsx index 79f5fbc90..4c4a5f1ad 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/integrations/marketplace.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/integrations/marketplace.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { BotId } from "@hazel/schema" import { createFileRoute } from "@tanstack/react-router" import { useCallback, useDeferredValue, useMemo, useState } from "react" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/invitations.tsx b/apps/web/src/routes/_app/$orgSlug/settings/invitations.tsx index 2f0424035..cec2fcfa8 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/invitations.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/invitations.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import type { InvitationId } from "@hazel/schema" import { IconArrowPath } from "~/components/icons/icon-arrow-path" import { eq, useLiveQuery } from "@tanstack/react-db" diff --git a/apps/web/src/routes/_app/$orgSlug/settings/team.tsx b/apps/web/src/routes/_app/$orgSlug/settings/team.tsx index 27a8796b8..5bff457ca 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/team.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/team.tsx @@ -1,4 +1,4 @@ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { UserId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { createFileRoute, useNavigate } from "@tanstack/react-router" diff --git a/apps/web/src/routes/_app/onboarding/setup-organization.tsx b/apps/web/src/routes/_app/onboarding/setup-organization.tsx index d9c2da572..73db80212 100644 --- a/apps/web/src/routes/_app/onboarding/setup-organization.tsx +++ b/apps/web/src/routes/_app/onboarding/setup-organization.tsx @@ -1,4 +1,4 @@ -import { useAtomSet } from "@effect-atom/atom-react" +import { useAtomSet } from "@effect/atom-react" import { eq, useLiveQuery } from "@tanstack/react-db" import { createFileRoute, Navigate, useNavigate } from "@tanstack/react-router" import { type } from "arktype" diff --git a/apps/web/src/routes/auth/callback.tsx b/apps/web/src/routes/auth/callback.tsx index 8c916687c..f16008398 100644 --- a/apps/web/src/routes/auth/callback.tsx +++ b/apps/web/src/routes/auth/callback.tsx @@ -4,7 +4,7 @@ * @description Receives OAuth callback from WorkOS, exchanges code for JWT tokens, and stores them in localStorage */ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { createFileRoute, useNavigate } from "@tanstack/react-router" import { Schema } from "effect" import { useEffect, useMemo } from "react" diff --git a/apps/web/src/routes/auth/desktop-callback.tsx b/apps/web/src/routes/auth/desktop-callback.tsx index 85dfad813..4f25b097e 100644 --- a/apps/web/src/routes/auth/desktop-callback.tsx +++ b/apps/web/src/routes/auth/desktop-callback.tsx @@ -4,7 +4,7 @@ * @description Receives OAuth callback from WorkOS and forwards to desktop app's local server */ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { DesktopAuthState } from "@hazel/domain/http" import { createFileRoute } from "@tanstack/react-router" import { Schema } from "effect" diff --git a/apps/web/src/routes/auth/desktop-login.tsx b/apps/web/src/routes/auth/desktop-login.tsx index d700e0c97..7b1fce534 100644 --- a/apps/web/src/routes/auth/desktop-login.tsx +++ b/apps/web/src/routes/auth/desktop-login.tsx @@ -4,7 +4,7 @@ * @description Login page for desktop app that initiates OAuth flow via system browser */ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { createFileRoute, Navigate, redirect } from "@tanstack/react-router" import { desktopAuthErrorAtom, diff --git a/apps/web/src/routes/join/$slug.tsx b/apps/web/src/routes/join/$slug.tsx index 5c37276d7..7a36624c9 100644 --- a/apps/web/src/routes/join/$slug.tsx +++ b/apps/web/src/routes/join/$slug.tsx @@ -1,4 +1,4 @@ -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react" +import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" import { createFileRoute, Link, useNavigate } from "@tanstack/react-router" import { Match } from "effect" import { motion } from "motion/react" diff --git a/bots/hazel-bot/package.json b/bots/hazel-bot/package.json index de540ac32..9b6015232 100644 --- a/bots/hazel-bot/package.json +++ b/bots/hazel-bot/package.json @@ -9,8 +9,6 @@ "dev": "bun run --watch src/index.ts" }, "dependencies": { - "@effect/ai": "catalog:effect", - "@effect/platform": "catalog:effect", "@hazel-chat/bot-sdk": "workspace:*", "@hazel/ai-openrouter": "workspace:*", "@hazel/domain": "workspace:*", diff --git a/bots/hazel-bot/src/agent-loop.ts b/bots/hazel-bot/src/agent-loop.ts index 01b197ae5..38beed039 100644 --- a/bots/hazel-bot/src/agent-loop.ts +++ b/bots/hazel-bot/src/agent-loop.ts @@ -1,4 +1,4 @@ -import { AiError, LanguageModel, Prompt, type Response, type Toolkit } from "@effect/ai" +import { AiError, LanguageModel, Prompt, type Response, type Toolkit } from "effect/unstable/ai" import { Duration, Effect, Mailbox, Stream } from "effect" import { withDegenerationDetection } from "./degeneration-detector.ts" diff --git a/bots/hazel-bot/src/degeneration-detector.ts b/bots/hazel-bot/src/degeneration-detector.ts index 8edc7d322..53a768959 100644 --- a/bots/hazel-bot/src/degeneration-detector.ts +++ b/bots/hazel-bot/src/degeneration-detector.ts @@ -1,4 +1,4 @@ -import type { Response } from "@effect/ai" +import type { Response } from "effect/unstable/ai" import { Effect, Stream } from "effect" import { DegenerateOutputError } from "./errors.ts" diff --git a/bots/hazel-bot/src/errors.ts b/bots/hazel-bot/src/errors.ts index f7ad2497a..ba4fde78b 100644 --- a/bots/hazel-bot/src/errors.ts +++ b/bots/hazel-bot/src/errors.ts @@ -1,13 +1,13 @@ import { Schema } from "effect" -export class StreamIdleTimeoutError extends Schema.TaggedError()( +export class StreamIdleTimeoutError extends Schema.TaggedErrorClass()( "StreamIdleTimeoutError", { message: Schema.String, }, ) {} -export class DegenerateOutputError extends Schema.TaggedError()( +export class DegenerateOutputError extends Schema.TaggedErrorClass()( "DegenerateOutputError", { message: Schema.String, @@ -16,13 +16,13 @@ export class DegenerateOutputError extends Schema.TaggedError()( +export class IterationTimeoutError extends Schema.TaggedErrorClass()( "IterationTimeoutError", { message: Schema.String, }, ) {} -export class SessionTimeoutError extends Schema.TaggedError()("SessionTimeoutError", { +export class SessionTimeoutError extends Schema.TaggedErrorClass()("SessionTimeoutError", { message: Schema.String, }) {} diff --git a/bots/hazel-bot/src/handler.ts b/bots/hazel-bot/src/handler.ts index 5ce83a603..0641bb850 100644 --- a/bots/hazel-bot/src/handler.ts +++ b/bots/hazel-bot/src/handler.ts @@ -1,4 +1,4 @@ -import { LanguageModel } from "@effect/ai" +import { LanguageModel } from "effect/unstable/ai" import { generateIntegrationInstructions, type AIContentChunk, diff --git a/bots/hazel-bot/src/openrouter.ts b/bots/hazel-bot/src/openrouter.ts index 7a1ce26cb..f41e67093 100644 --- a/bots/hazel-bot/src/openrouter.ts +++ b/bots/hazel-bot/src/openrouter.ts @@ -1,5 +1,5 @@ import { OpenRouterClient, OpenRouterLanguageModel } from "@hazel/ai-openrouter" -import { FetchHttpClient } from "@effect/platform" +import { FetchHttpClient } from "effect/unstable/http" import { Config, Effect, Layer } from "effect" const OpenRouterClientLayer = OpenRouterClient.layerConfig({ diff --git a/bots/hazel-bot/src/stream.ts b/bots/hazel-bot/src/stream.ts index 290657271..e5544dece 100644 --- a/bots/hazel-bot/src/stream.ts +++ b/bots/hazel-bot/src/stream.ts @@ -1,5 +1,5 @@ import type { AIContentChunk } from "@hazel-chat/bot-sdk" -import type { Response } from "@effect/ai" +import type { Response } from "effect/unstable/ai" import { Match } from "effect" export const mapEffectPartToChunk: (part: Response.AnyPart) => AIContentChunk | null = diff --git a/bots/hazel-bot/src/tools/base.ts b/bots/hazel-bot/src/tools/base.ts index 8ea786b1a..409de4eff 100644 --- a/bots/hazel-bot/src/tools/base.ts +++ b/bots/hazel-bot/src/tools/base.ts @@ -1,4 +1,4 @@ -import { Tool } from "@effect/ai" +import { Tool } from "effect/unstable/ai" import { Schema } from "effect" export const GetCurrentTime = Tool.make("get_current_time", { diff --git a/bots/hazel-bot/src/tools/craft.ts b/bots/hazel-bot/src/tools/craft.ts index 3d5bfa320..3f39dd0eb 100644 --- a/bots/hazel-bot/src/tools/craft.ts +++ b/bots/hazel-bot/src/tools/craft.ts @@ -1,4 +1,4 @@ -import { Tool } from "@effect/ai" +import { Tool } from "effect/unstable/ai" import { Schema } from "effect" export const CraftSearchDocuments = Tool.make("craft_search_documents", { diff --git a/bots/hazel-bot/src/tools/linear.ts b/bots/hazel-bot/src/tools/linear.ts index fdb290c92..4a264b26f 100644 --- a/bots/hazel-bot/src/tools/linear.ts +++ b/bots/hazel-bot/src/tools/linear.ts @@ -1,4 +1,4 @@ -import { Tool } from "@effect/ai" +import { Tool } from "effect/unstable/ai" import { Schema } from "effect" export const LinearGetAccountInfo = Tool.make("linear_get_account_info", { diff --git a/bots/hazel-bot/src/tools/toolkit.ts b/bots/hazel-bot/src/tools/toolkit.ts index 952e02132..24c692018 100644 --- a/bots/hazel-bot/src/tools/toolkit.ts +++ b/bots/hazel-bot/src/tools/toolkit.ts @@ -1,4 +1,4 @@ -import { Toolkit } from "@effect/ai" +import { Toolkit } from "effect/unstable/ai" import { LinearApiClient, makeLinearSdkClient } from "@hazel/integrations/linear" import { CraftApiClient } from "@hazel/integrations/craft" import type { IntegrationConnection } from "@hazel/domain/models" diff --git a/bots/linear-bot/package.json b/bots/linear-bot/package.json index e99fcc3f5..a0542010c 100644 --- a/bots/linear-bot/package.json +++ b/bots/linear-bot/package.json @@ -9,8 +9,6 @@ "dev": "bun run --watch src/index.ts" }, "dependencies": { - "@effect/ai": "catalog:effect", - "@effect/platform": "catalog:effect", "@hazel-chat/bot-sdk": "workspace:*", "@hazel/ai-openrouter": "workspace:*", "@hazel/db": "workspace:*", diff --git a/bots/linear-bot/src/index.ts b/bots/linear-bot/src/index.ts index 667a25a23..6a57de2e2 100644 --- a/bots/linear-bot/src/index.ts +++ b/bots/linear-bot/src/index.ts @@ -1,6 +1,6 @@ -import { LanguageModel } from "@effect/ai" +import { LanguageModel } from "effect/unstable/ai" import { OpenRouterClient, OpenRouterLanguageModel } from "@hazel/ai-openrouter" -import { FetchHttpClient } from "@effect/platform" +import { FetchHttpClient } from "effect/unstable/http" import { Config, Effect, Layer, Schema } from "effect" import { runHazelBot } from "@hazel-chat/bot-sdk" import { LinearApiClient } from "@hazel/integrations/linear" diff --git a/bun.lock b/bun.lock index 2434fe79e..394b17308 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "maki-chat", "devDependencies": { - "@effect/vitest": "^0.27.0", + "@effect/vitest": "catalog:effect", "@rolldown/plugin-babel": "^0.2.1", "@vitest/coverage-v8": "^4.1.0", "oxfmt": "^0.40.0", @@ -14,7 +14,7 @@ "turbo": "^2.8.16", "typescript": "^5.9.3", "vitest": "^4.1.0", - "web": "^0.0.2", + "web": "workspace:*", "wrangler": "^4.73.0", }, }, @@ -35,14 +35,9 @@ "apps/backend": { "name": "@hazel/backend", "dependencies": { - "@effect/cluster": "catalog:effect", - "@effect/experimental": "catalog:effect", "@effect/opentelemetry": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-bun": "catalog:effect", "@effect/platform-node": "catalog:effect", - "@effect/rpc": "catalog:effect", - "@effect/sql": "catalog:effect", "@effect/sql-pg": "catalog:effect", "@hazel/auth": "workspace:*", "@hazel/backend-core": "workspace:*", @@ -59,7 +54,6 @@ "pg": "^8.16.3", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@testcontainers/postgresql": "^10.18.0", "@types/bun": "1.3.9", "drizzle-kit": "^0.31.8", @@ -78,7 +72,6 @@ "effect": "catalog:effect", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3", }, @@ -86,13 +79,8 @@ "apps/cluster": { "name": "cluster", "dependencies": { - "@effect/ai": "catalog:effect", - "@effect/cluster": "catalog:effect", - "@effect/experimental": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-bun": "catalog:effect", "@effect/sql-pg": "catalog:effect", - "@effect/workflow": "catalog:effect", "@hazel/ai-openrouter": "workspace:*", "@hazel/backend-core": "workspace:*", "@hazel/db": "workspace:*", @@ -163,8 +151,6 @@ "name": "@hazel/electric-proxy", "version": "0.0.0", "dependencies": { - "@effect/experimental": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-bun": "catalog:effect", "@electric-sql/client": "1.5.12", "@hazel/auth": "workspace:*", @@ -221,7 +207,6 @@ "name": "@hazel/link-preview-worker", "version": "0.0.0", "dependencies": { - "@effect/platform": "catalog:effect", "effect": "catalog:effect", "metascraper": "^5.49.5", "metascraper-description": "^5.49.5", @@ -234,7 +219,6 @@ }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.8.19", - "@effect/language-service": "catalog:effect", "typescript": "^5.9.3", "vitest": "^4.1.0", "wrangler": "^4.73.0", @@ -245,12 +229,9 @@ "dependencies": { "@cloudflare/realtimekit-react": "^1.2.4", "@cloudflare/realtimekit-react-ui": "^1.1.0", - "@effect-atom/atom-react": "catalog:effect", - "@effect/experimental": "catalog:effect", + "@effect/atom-react": "catalog:effect", "@effect/opentelemetry": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-browser": "catalog:effect", - "@effect/rpc": "catalog:effect", "@electric-sql/client": "1.5.12", "@fontsource/inter": "^5.2.8", "@hazel/actors": "workspace:*", @@ -327,7 +308,6 @@ "workbox-window": "^7.4.0", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@react-types/overlays": "^3.9.4", "@react-types/shared": "^3.33.1", "@tailwindcss/postcss": "^4.2.1", @@ -355,8 +335,6 @@ "name": "hazel-bot", "version": "1.0.0", "dependencies": { - "@effect/ai": "catalog:effect", - "@effect/platform": "catalog:effect", "@hazel-chat/bot-sdk": "workspace:*", "@hazel/ai-openrouter": "workspace:*", "@hazel/domain": "workspace:*", @@ -372,8 +350,6 @@ "name": "linear-bot", "version": "1.0.0", "dependencies": { - "@effect/ai": "catalog:effect", - "@effect/platform": "catalog:effect", "@hazel-chat/bot-sdk": "workspace:*", "@hazel/ai-openrouter": "workspace:*", "@hazel/db": "workspace:*", @@ -390,13 +366,10 @@ "name": "@hazel/ai-openrouter", "version": "0.0.1", "dependencies": { - "@effect/ai": "catalog:effect", - "@effect/experimental": "catalog:effect", - "@effect/platform": "catalog:effect", + "@effect/ai-openrouter": "catalog:effect", "effect": "catalog:effect", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "typescript": "^5.9.3", }, }, @@ -404,11 +377,8 @@ "name": "@hazel-chat/bot-sdk", "version": "0.1.0", "devDependencies": { - "@effect/language-service": "catalog:effect", "@effect/opentelemetry": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-bun": "catalog:effect", - "@effect/rpc": "catalog:effect", "@hazel/actors": "workspace:*", "@hazel/domain": "workspace:*", "@hazel/effect-bun": "workspace:*", @@ -422,18 +392,14 @@ "typescript": "^5.9.3", }, "peerDependencies": { - "@effect/opentelemetry": ">=0.61.0", - "@effect/platform": ">=0.94.0", - "@effect/platform-bun": ">=0.87.0", - "@effect/rpc": ">=0.73.0", - "@effect/workflow": ">=0.16.0", - "effect": ">=3.19.0", + "@effect/opentelemetry": ">=4.0.0-beta.0", + "@effect/platform-bun": ">=4.0.0-beta.0", + "effect": ">=4.0.0-beta.0", "jose": ">=6.0.0", "rivetkit": ">=2.0.0", }, "optionalPeers": [ "@effect/opentelemetry", - "@effect/workflow", "jose", "rivetkit", ], @@ -442,7 +408,6 @@ "name": "@hazel/effect-electric-db-collection", "version": "0.0.0", "dependencies": { - "@effect-atom/atom": "catalog:effect", "@electric-sql/client": "1.5.12", "@standard-schema/spec": "^1.1.0", "@tanstack/db": "0.5.31", @@ -452,7 +417,6 @@ "effect": "catalog:effect", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@vitest/coverage-istanbul": "^4.0.17", "typescript": "^5.9.3", }, @@ -461,12 +425,11 @@ "name": "@hazel/tanstack-db-atom", "version": "0.0.0", "dependencies": { - "@effect-atom/atom-react": "catalog:effect", + "@effect/atom-react": "catalog:effect", "@tanstack/db": "0.5.31", "effect": "catalog:effect", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "typescript": "^5.9.3", }, }, @@ -474,7 +437,6 @@ "name": "@hazel/actors", "version": "0.0.1", "dependencies": { - "@effect/platform": "catalog:effect", "@hazel/rivet-effect": "workspace:*", "@hazel/schema": "workspace:*", "effect": "catalog:effect", @@ -492,8 +454,6 @@ "name": "@hazel/auth", "version": "0.0.0", "dependencies": { - "@effect/experimental": "catalog:effect", - "@effect/platform": "catalog:effect", "@hazel/db": "workspace:*", "@hazel/domain": "workspace:*", "@hazel/effect-bun": "workspace:*", @@ -503,7 +463,6 @@ "jose": "^6.1.3", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3", }, @@ -512,7 +471,6 @@ "name": "@hazel/backend-core", "version": "0.0.0", "dependencies": { - "@effect/platform": "catalog:effect", "@hazel/db": "workspace:*", "@hazel/domain": "workspace:*", "@hazel/schema": "workspace:*", @@ -521,7 +479,6 @@ "effect": "catalog:effect", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3", }, @@ -534,7 +491,6 @@ "name": "@hazel/db", "version": "0.0.0", "dependencies": { - "@effect/experimental": "catalog:effect", "@hazel/domain": "workspace:*", "@hazel/schema": "workspace:*", "drizzle-orm": "^0.45.1", @@ -542,7 +498,6 @@ "postgres": "^3.4.7", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@testcontainers/postgresql": "^10.18.0", "@types/bun": "1.3.9", "drizzle-kit": "^0.31.8", @@ -553,18 +508,12 @@ "name": "@hazel/domain", "version": "0.0.0", "dependencies": { - "@effect/cluster": "catalog:effect", - "@effect/experimental": "catalog:effect", - "@effect/platform": "catalog:effect", - "@effect/rpc": "catalog:effect", - "@effect/workflow": "catalog:effect", "@hazel/integrations": "workspace:*", "@hazel/schema": "workspace:*", "drizzle-orm": "^0.45.1", "effect": "catalog:effect", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3", }, @@ -573,14 +522,11 @@ "name": "@hazel/effect-bun", "version": "0.0.0", "dependencies": { - "@effect/experimental": "catalog:effect", "@effect/opentelemetry": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-bun": "catalog:effect", "effect": "catalog:effect", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "latest", "typescript": "^5.9.3", }, @@ -589,7 +535,6 @@ "name": "@hazel/integrations", "version": "0.0.0", "dependencies": { - "@effect/platform": "catalog:effect", "@linear/sdk": "^73.0.0", "effect": "catalog:effect", "he": "^1.2.0", @@ -597,7 +542,6 @@ "rss-parser": "^3.13.0", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "@types/he": "^1.2.3", "typescript": "^5.9.3", @@ -611,8 +555,7 @@ "rivetkit": "2.1.6", }, "devDependencies": { - "@effect/language-service": "catalog:effect", - "@effect/vitest": "^0.27.0", + "@effect/vitest": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3", }, @@ -621,11 +564,9 @@ "name": "@hazel/schema", "version": "0.0.0", "dependencies": { - "@effect/platform": "catalog:effect", "effect": "catalog:effect", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3", }, @@ -637,11 +578,7 @@ "hazel-setup": "./src/index.ts", }, "dependencies": { - "@effect/cli": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-bun": "catalog:effect", - "@effect/printer": "catalog:effect", - "@effect/printer-ansi": "catalog:effect", "@hazel/db": "workspace:*", "@hazel/schema": "workspace:*", "@workos-inc/node": "^7.77.0", @@ -649,7 +586,6 @@ "picocolors": "^1.1.1", }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3", }, @@ -668,25 +604,15 @@ }, "catalogs": { "effect": { - "@effect-atom/atom": "0.5.3", - "@effect-atom/atom-react": "0.5.0", - "@effect/ai": "0.33.2", - "@effect/cli": "0.73.2", - "@effect/cluster": "0.56.4", - "@effect/experimental": "0.58.0", - "@effect/language-service": "0.79.0", - "@effect/opentelemetry": "0.61.0", - "@effect/platform": "0.94.5", - "@effect/platform-browser": "0.74.0", - "@effect/platform-bun": "0.87.1", - "@effect/platform-node": "0.104.1", - "@effect/printer": "0.47.0", - "@effect/printer-ansi": "0.47.0", - "@effect/rpc": "0.73.2", - "@effect/sql": "0.49.0", - "@effect/sql-pg": "0.50.3", - "@effect/workflow": "0.16.0", - "effect": "3.19.19", + "@effect/ai-openrouter": "4.0.0-beta.32", + "@effect/atom-react": "4.0.0-beta.32", + "@effect/opentelemetry": "4.0.0-beta.32", + "@effect/platform-browser": "4.0.0-beta.32", + "@effect/platform-bun": "4.0.0-beta.32", + "@effect/platform-node": "4.0.0-beta.32", + "@effect/sql-pg": "4.0.0-beta.32", + "@effect/vitest": "4.0.0-beta.32", + "effect": "4.0.0-beta.32", }, }, "packages": { @@ -980,47 +906,27 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - "@effect-atom/atom": ["@effect-atom/atom@0.5.3", "", { "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "effect": "^3.19.15" } }, "sha512-TRZv/i+YT3TtnN0oFORJqXdxSs1fc7lrJlH+1xZvDFyjC9hgoVnrcKbeZsDFmr6r0wYRqVo7U3IftxiQNjpNZA=="], - - "@effect-atom/atom-react": ["@effect-atom/atom-react@0.5.0", "", { "dependencies": { "@effect-atom/atom": "^0.5.0" }, "peerDependencies": { "effect": "^3.19", "react": ">=18 <20", "scheduler": "*" } }, "sha512-aFfjWi4rEJCqfM12Oi36/EKaDm/W6n4/N6yM5vL0t/QozKhJhK05rQL/GY4XMxlH2eqkQ4ih8jBQa3Yyp0Fiqw=="], - - "@effect/ai": ["@effect/ai@0.33.2", "", { "dependencies": { "find-my-way-ts": "^0.1.6" }, "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.1", "@effect/rpc": "^0.73.0", "effect": "^3.19.14" } }, "sha512-iJ6pz9qiKFXhcnriJJSBQ5+Bz3oEg+6hVQ7OPmmTa0dVkKUPIgX9nHqHfstcwLnDfdXBj/zCl79U+iMpGtQBnA=="], - - "@effect/cli": ["@effect/cli@0.73.2", "", { "dependencies": { "ini": "^4.1.3", "toml": "^3.0.0", "yaml": "^2.5.0" }, "peerDependencies": { "@effect/platform": "^0.94.3", "@effect/printer": "^0.47.0", "@effect/printer-ansi": "^0.47.0", "effect": "^3.19.16" } }, "sha512-K8IJo81+qa1LU8dhxcDU4QO/bIjL/dPd3zUOSCpLiuUNz8Y3/T+WNs3GqIXEhMfCFMSlRZERN0YgmtRlEZUREA=="], - - "@effect/cluster": ["@effect/cluster@0.56.4", "", { "dependencies": { "kubernetes-types": "^1.30.0" }, "peerDependencies": { "@effect/platform": "^0.94.5", "@effect/rpc": "^0.73.1", "@effect/sql": "^0.49.0", "@effect/workflow": "^0.16.0", "effect": "^3.19.17" } }, "sha512-7Je5/JlbZOlsSxsbKjr97dJed2cNGWsb+TLNgMcr5mRDbcWlFOTUGvsrisEJV6waosYLIg+2omPdvnvRoYKdhA=="], - - "@effect/experimental": ["@effect/experimental@0.58.0", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.94.0", "effect": "^3.19.13", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-IEP9sapjF6rFy5TkoqDPc86st/fnqUfjT7Xa3pWJrFGr1hzaMXHo+mWsYOZS9LAOVKnpHuVziDK97EP5qsCHVA=="], + "@effect/ai-openrouter": ["@effect/ai-openrouter@4.0.0-beta.32", "", { "peerDependencies": { "effect": "^4.0.0-beta.32" } }, "sha512-WSK9S40cq50U8I0RJTHjRMubV/WevdleSc7XF9OJXMVaxrUgN/2oLTMEQOopnb/Gk/0bUtjLYxw7XKJ5zy7rDQ=="], - "@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="], + "@effect/atom-react": ["@effect/atom-react@4.0.0-beta.32", "", { "peerDependencies": { "effect": "^4.0.0-beta.32", "react": "^19.2.4", "scheduler": "*" } }, "sha512-xft2Fn8RRI6sccqM6iCq1EmYT9K9e/q4rpNJCEpj7rEljZZBwkJBoet7R64OrDZFyT9COys+8M07pegLS3q69g=="], - "@effect/opentelemetry": ["@effect/opentelemetry@0.61.0", "", { "peerDependencies": { "@effect/platform": "^0.94.2", "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^3.19.15" } }, "sha512-aTg4qWzMxGDoUZKSyws/dMZqVNoSCQIvHaPDJnWitUAVuWDAPOXiaiHJDWO39cwDlySBTCSF6rEanm3+nR/2Mg=="], + "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.32", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.32" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-NsR7vJBXDbYr+8fcAaKUyGC022s7lTeUODPD8kabqOlVrs86k/LNcTtVtGXUAjY4o1u/c3kipJ0p4z9rCniwsQ=="], - "@effect/platform": ["@effect/platform@0.94.5", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.17" } }, "sha512-z05APUiDDPbodhTkH/RJqOLoCU11bU2IZLfcwLFrld03+ob1VeqRnELQlmueLIYm6NZifHAtjl32V+GRt34y4A=="], + "@effect/platform": ["@effect/platform@0.94.3", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.16" } }, "sha512-bvTR8xLQoRpKgHuprZDOeQdPkhyVw+WT05iI9jl2s8Qiblyk5Dz2JLwJU+EFeksIBaPYz49xa635Om91T1CefQ=="], - "@effect/platform-browser": ["@effect/platform-browser@0.74.0", "", { "dependencies": { "multipasta": "^0.2.7" }, "peerDependencies": { "@effect/platform": "^0.94.0", "effect": "^3.19.13" } }, "sha512-PAgkg5L5cASQpScA0SZTSy543MVA4A9kmpVCjo2fCINLRpTeuCFAOQHgPmw8dKHnYS0yGs2TYn7AlrhhqQ5o3g=="], + "@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.32", "", { "dependencies": { "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^4.0.0-beta.32" } }, "sha512-sV5d/Qs6y74DOcIQe4QaCrNhqVwRvZfi13cQAMmnv7xX+El7TDn0EZ0i8xmT4mCF2KrG3mHKHCwKoDOIHgEcxg=="], - "@effect/platform-bun": ["@effect/platform-bun@0.87.1", "", { "dependencies": { "@effect/platform-node-shared": "^0.57.1", "multipasta": "^0.2.7" }, "peerDependencies": { "@effect/cluster": "^0.56.1", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "effect": "^3.19.15" } }, "sha512-I88d0YqWbvLY2GGeIxK3r5k0l/MoUCCnxiHJG+X6gqaHu+pIs0djDtJ+ORhw/3qha9ojcVu6pyaBmnUjgzQHWQ=="], + "@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.32", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.32" }, "peerDependencies": { "effect": "^4.0.0-beta.32" } }, "sha512-/za6Ps9sh6S3or9OxfR1LIxseVuw/3E4qbtcs6s4nMFzrLyh8bGxp4rqqUnQ1pZxBV8i/eziHpg9M8xOghlLkQ=="], - "@effect/platform-node": ["@effect/platform-node@0.104.1", "", { "dependencies": { "@effect/platform-node-shared": "^0.57.1", "mime": "^3.0.0", "undici": "^7.10.0", "ws": "^8.18.2" }, "peerDependencies": { "@effect/cluster": "^0.56.1", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "effect": "^3.19.15" } }, "sha512-jT1a/z98niK6fnEU8pWHPPCdJMVDRCIdB65lolcOjse5rsTwVbczMjvKkhVQpF63mNWoOnol7OTRNkw5L54llg=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.32", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.32", "mime": "^4.1.0", "undici": "^7.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.32", "ioredis": "^5.7.0" } }, "sha512-rgMQbbevkoQsrvVvbEe9qSU2woMXYSDLlO8aZEZ0GgXROEZC7r2tO5JL+BANKSCvauE916uYrSxwMovLBcRbMg=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@0.57.1", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "multipasta": "^0.2.7", "ws": "^8.18.2" }, "peerDependencies": { "@effect/cluster": "^0.56.1", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "effect": "^3.19.15" } }, "sha512-oX/bApMdoKsyrDiNdJxo7U9Rz1RXsjRv+ecfAPp1qGlSdGIo32wVRvJ2XCHqYj0sqaYJS0pU0/GCulRfVGuJag=="], - - "@effect/printer": ["@effect/printer@0.47.0", "", { "peerDependencies": { "@effect/typeclass": "^0.38.0", "effect": "^3.19.0" } }, "sha512-VgR8e+YWWhMEAh9qFOjwiZ3OXluAbcVLIOtvp2S5di1nSrPOZxj78g8LE77JSvyfp5y5bS2gmFW+G7xD5uU+2Q=="], - - "@effect/printer-ansi": ["@effect/printer-ansi@0.47.0", "", { "dependencies": { "@effect/printer": "^0.47.0" }, "peerDependencies": { "@effect/typeclass": "^0.38.0", "effect": "^3.19.0" } }, "sha512-tDEQ9XJpXDNYoWMQJHFRMxKGmEOu6z32x3Kb8YLOV5nkauEKnKmWNs7NBp8iio/pqoJbaSwqDwUg9jXVquxfWQ=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.32", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.32" } }, "sha512-gpEV2uPom22DbqobiBK75em7Jo6wW4sti7YA2vD1IkZPmOpC7snpB50X7CgeM9NlRkbaqC7xbKztoNk+/BBJkA=="], "@effect/rpc": ["@effect/rpc@0.73.2", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.94.5", "effect": "^3.19.18" } }, "sha512-td7LHDgBOYKg+VgGWEelD8rSAmvjXz7am17vfxZROX5qIYuvH7drL/z4p5xQFadhHZ7DYdlFpqdO9ggc77OCIw=="], - "@effect/sql": ["@effect/sql@0.49.0", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.0", "effect": "^3.19.13" } }, "sha512-9UEKR+z+MrI/qMAmSvb/RiD9KlgIazjZUCDSpwNgm0lEK9/Q6ExEyfziiYFVCPiptp52cBw8uBHRic8hHnwqXA=="], - - "@effect/sql-pg": ["@effect/sql-pg@0.50.3", "", { "dependencies": { "pg": "^8.16.3", "pg-connection-string": "2.9.1", "pg-cursor": "^2.15.3", "pg-pool": "^3.10.1", "pg-types": "^4.1.0" }, "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.4", "@effect/sql": "^0.49.0", "effect": "^3.19.16" } }, "sha512-mKY8+qJFWvmMeX+TA2j1r01XXnI23Z5gu0KI1jDgjLATNImPXWsPIb1bnoZTyUNW5CIUu6X/wFT6M+8sZYLCiA=="], - - "@effect/typeclass": ["@effect/typeclass@0.38.0", "", { "peerDependencies": { "effect": "^3.19.0" } }, "sha512-lMUcJTRtG8KXhXoczapZDxbLK5os7M6rn0zkvOgncJW++A0UyelZfMVMKdT5R+fgpZcsAU/1diaqw3uqLJwGxA=="], - - "@effect/vitest": ["@effect/vitest@0.27.0", "", { "peerDependencies": { "effect": "^3.19.0", "vitest": "^3.2.0" } }, "sha512-8bM7n9xlMUYw9GqPIVgXFwFm2jf27m/R7psI64PGpwU5+26iwyxp9eAXEsfT5S6lqztYfpQQ1Ubp5o6HfNYzJQ=="], + "@effect/sql-pg": ["@effect/sql-pg@4.0.0-beta.32", "", { "dependencies": { "pg": "^8.18.0", "pg-connection-string": "2.11.0", "pg-cursor": "^2.17.0", "pg-pool": "^3.11.0", "pg-types": "^4.1.0" }, "peerDependencies": { "effect": "^4.0.0-beta.32" } }, "sha512-GGL+uc3vouBnrHCOvz2M8OE9FevG0D2W2kUDsHL0uv1rDdAXAYpP41WKb1+hZNfydN0WVQs71Jxdtlaq3ZDSAQ=="], - "@effect/workflow": ["@effect/workflow@0.16.0", "", { "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.0", "@effect/rpc": "^0.73.0", "effect": "^3.19.13" } }, "sha512-MiAdlxx3TixkgHdbw+Yf1Z3tHAAE0rOQga12kIydJqj05Fnod+W/I+kQGRMY/XWRg+QUsVxhmh1qTr7Ype6lrw=="], + "@effect/vitest": ["@effect/vitest@4.0.0-beta.32", "", { "peerDependencies": { "effect": "^4.0.0-beta.32", "vitest": "^3.0.0 || ^4.0.0" } }, "sha512-ZXzvFtS2skcJx8vL/pkBvFt2Yxd/FScP3r6EGkCoI94jL9ddd4rcnt8JwhyBQQLiW26+aj5gDnl+gCotjBrruA=="], "@electric-sql/client": ["@electric-sql/client@1.5.12", "", { "dependencies": { "@microsoft/fetch-event-source": "^2.0.1" }, "optionalDependencies": { "@rollup/rollup-darwin-arm64": "^4.18.1" }, "bin": { "intent": "bin/intent.mjs" } }, "sha512-mWDEpKog0Zo4WOjReW4x9ELaHsjTthpziLVJkjmSdh/4Y/ZHw5EoY5e9dJX9itPSKVk9GNFz3YfHFv5EAyCOmw=="], @@ -1254,6 +1160,8 @@ "@internationalized/string": ["@internationalized/string@3.2.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A=="], + "@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], @@ -1332,9 +1240,7 @@ "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], - "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.5.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw=="], - - "@opentelemetry/core": ["@opentelemetry/core@2.4.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw=="], + "@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg=="], @@ -1342,7 +1248,7 @@ "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], - "@opentelemetry/resources": ["@opentelemetry/resources@2.4.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg=="], + "@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="], "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], @@ -1350,10 +1256,6 @@ "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.5.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.5.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/sdk-trace-base": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-O6N/ejzburFm2C84aKNrwJVPpt6HSTSq8T0ZUMq3xT2XmqT4cwxUItcL5UWGThYuq8RTcbH8u1sfj6dmRci0Ow=="], - - "@opentelemetry/sdk-trace-web": ["@opentelemetry/sdk-trace-web@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/sdk-trace-base": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-xWibakHs+xbx6vxH7Q8TbFS6zjf812o/kIS4xBDB32qSL9wF+Z5IZl2ZAGu4rtmPBQ7coZcOd684DobMhf8dKw=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="], "@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="], @@ -1524,34 +1426,6 @@ "@paper-design/shaders-react": ["@paper-design/shaders-react@0.0.71", "", { "dependencies": { "@paper-design/shaders": "0.0.71" }, "peerDependencies": { "@types/react": "^18 || ^19", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-kTqjIlyZcpkwqJie+3ldEDscTtx1oOi8eRBD5QgWKI21GaNn/SSg26092M5zzqr3e8dVANv0ktS2ICSjbMFKbw=="], - "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], - - "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="], - - "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="], - - "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="], - - "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="], - - "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="], - - "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="], - - "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="], - - "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="], - - "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="], - - "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="], - - "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="], - - "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="], - - "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], - "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.5.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ=="], "@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="], @@ -2302,6 +2176,8 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@untitledui/file-icons": ["@untitledui/file-icons@0.0.9", "", { "peerDependencies": { "react": ">= 18" } }, "sha512-wz3btNnSSv2hTujgyxEFL21oyjPtGTj3osU8ZEMe8nwdlmLQEILLe96NMg9b1ciiOC5UOWNDeYIt7IfxEM6qtg=="], @@ -2574,6 +2450,8 @@ "cluster": ["cluster@workspace:apps/cluster"], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "cobe": ["cobe@0.6.5", "", { "dependencies": { "phenomenon": "^1.6.0" } }, "sha512-MA8bu81EFY6JjQpj+FovEuhyJ25khx2Q7Lh+ot/UkCJe5yKyDgzdc6u2lGZIOmsZTXK6Itg1i4lQZIJZbPWnAg=="], "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], @@ -2688,6 +2566,8 @@ "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], @@ -2746,7 +2626,7 @@ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - "effect": ["effect@3.19.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg=="], + "effect": ["effect@4.0.0-beta.32", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-22DEm3JB9njSJzYxBXA6h38JHIxLu6JpKQmhQw2C28gwptpnQXc4Ba3bIQubqsKSzYqNjp5fz9YdOZuKSwxkXQ=="], "effect-rpc-tanstack-devtools": ["effect-rpc-tanstack-devtools@0.1.1", "", { "peerDependencies": { "@effect/rpc": ">=0.70.0", "@tanstack/devtools-event-client": ">=0.3.0", "effect": ">=3.0.0", "react": ">=18.0.0" } }, "sha512-iYddWCEwdUCquNrXexHfOzKPxLR5fD5HalSTgRnr7cW3aRPcTAjQ1XGXel0nJBeUoPcSkiEIuvDVsYYySviLyg=="], @@ -2850,7 +2730,7 @@ "facehash": ["facehash@0.1.0", "", { "peerDependencies": { "@types/react": "", "next": ">=15", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@types/react", "next"] }, "sha512-tv/QVZjLvEXHssqBaJECq+kRLFwwhd017PKk8ucT7aLingL2OZ5zEqKwPMHmT9+YQO92MVFWGZQP6vxV+P5vrQ=="], - "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fast-check": ["fast-check@4.6.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -3054,7 +2934,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="], + "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], @@ -3068,6 +2948,8 @@ "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], + "ioredis": ["ioredis@5.10.0", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "ip-regex": ["ip-regex@4.3.0", "", {}, "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q=="], @@ -3272,6 +3154,10 @@ "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], + + "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], + "lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -3428,11 +3314,9 @@ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "microsoft-capitalize": ["microsoft-capitalize@1.0.5", "", {}, "sha512-iqDMU9J643BHg8Zp7EMZNLTp6Pgs2f1S2SMnCW2VlUqMs17xCZ5vwVjalBJEGVcUfG+/1ePqeEGcMW3VfzHK5A=="], - "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], "miniflare": ["miniflare@4.20260312.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260312.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-pieP2rfXynPT6VRINYaiHe/tfMJ4c5OIhqRlIdLF6iZ9g5xgpEmvimvIgMpgAdDJuFlrLcwDUi8MfAo2R6dt/w=="], @@ -3466,7 +3350,7 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], + "msgpackr": ["msgpackr@1.11.9", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw=="], "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], @@ -3496,8 +3380,6 @@ "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], - "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], - "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="], @@ -3604,15 +3486,15 @@ "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], - "pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="], + "pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="], - "pg-cursor": ["pg-cursor@2.15.3", "", { "peerDependencies": { "pg": "^8" } }, "sha512-eHw63TsiGtFEfAd7tOTZ+TLy+i/2ePKS20H84qCQ+aQ60pve05Okon9tKMC+YN3j6XyeFoHnaim7Lt9WVafQsA=="], + "pg-cursor": ["pg-cursor@2.19.0", "", { "peerDependencies": { "pg": "^8" } }, "sha512-J5cF1MUz7LRJ9emOqF/06QjabMHMZy587rSPF0UuA8rCwKeeYl2co8Pp+6k5UU9YrAYHMzWkLxilfZB0hqsWWw=="], "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], "pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="], - "pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], + "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="], "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], @@ -3694,7 +3576,7 @@ "punycode2": ["punycode2@1.0.1", "", {}, "sha512-+TXpd9YRW4YUZZPoRHJ3DILtWwootGc2DsgvfHmklQ8It1skINAuqSdqizt5nlTaBmwrYACHkHApCXjc9gHk2Q=="], - "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "pure-rand": ["pure-rand@8.0.0", "", {}, "sha512-7rgWlxG2gAvFPIQfUreo1XYlNvrQ9VnQPFWdncPkdl3icucLK0InOxsaafbvxGTnI6Bk/Rxmslg0lQlRCuzOXw=="], "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], @@ -3760,6 +3642,10 @@ "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], + + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], @@ -3968,6 +3854,8 @@ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], @@ -4148,7 +4036,7 @@ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - "undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], + "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -4216,7 +4104,7 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@12.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-USe1zesMYh4fjCA8ZH5+X5WIVD0J4V1Jksm1bFTVBX2F/cwSXt0RO5w/3UXbdLKmZX65MiWV+hwhSS8p6oBTGA=="], + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], "vbare": ["vbare@0.0.4", "", {}, "sha512-QsxSVw76NqYUWYPVcQmOnQPX8buIVjgn+yqldTHlWISulBTB9TJ9rnzZceDu+GZmycOtzsmuPbPN1YNxvK12fg=="], @@ -4540,11 +4428,19 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@effect-atom/atom-react/@effect-atom/atom": ["@effect-atom/atom@0.5.0", "", { "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "effect": "^3.19.15" } }, "sha512-qg6Qpf+ESi63taFJrufPtYDHghRLXxAte/xgpKqN+m7T6I3Lw1jgiNxR4AJeNG7f2UOkpmPhA8AYdWMgaUU61w=="], + "@effect/platform/effect": ["effect@3.19.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg=="], + + "@effect/platform/msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], + + "@effect/platform-node-shared/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "@effect/experimental/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "@effect/rpc/@effect/platform": ["@effect/platform@0.94.5", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.17" } }, "sha512-z05APUiDDPbodhTkH/RJqOLoCU11bU2IZLfcwLFrld03+ob1VeqRnELQlmueLIYm6NZifHAtjl32V+GRt34y4A=="], - "@effect/sql/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "@effect/rpc/effect": ["effect@3.19.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg=="], + + "@effect/rpc/msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], + + "@effect/sql-pg/pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], @@ -4576,38 +4472,20 @@ "@metascraper/helpers/jsdom": ["jsdom@27.0.1", "", { "dependencies": { "@asamuzakjp/dom-selector": "^6.7.2", "cssstyle": "^5.3.1", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.1.0", "ws": "^8.18.3", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA=="], - "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/otlp-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + "@metascraper/helpers/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], - "@opentelemetry/otlp-transformer/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - "@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], - - "@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ=="], - - "@opentelemetry/sdk-trace-web/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], - - "@opentelemetry/sdk-trace-web/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ=="], - - "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], - "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -4618,6 +4496,8 @@ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@rivetkit/engine-runner/uuid": ["uuid@12.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-USe1zesMYh4fjCA8ZH5+X5WIVD0J4V1Jksm1bFTVBX2F/cwSXt0RO5w/3UXbdLKmZX65MiWV+hwhSS8p6oBTGA=="], + "@rollup/plugin-babel/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], "@rollup/plugin-babel/@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="], @@ -4830,7 +4710,7 @@ "cssstyle/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], - "dfx/@effect/platform": ["@effect/platform@0.94.3", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.16" } }, "sha512-bvTR8xLQoRpKgHuprZDOeQdPkhyVw+WT05iI9jl2s8Qiblyk5Dz2JLwJU+EFeksIBaPYz49xa635Om91T1CefQ=="], + "dfx/effect": ["effect@3.19.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg=="], "docker-modem/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -4842,6 +4722,8 @@ "drizzle-kit/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], + "effect-rpc-tanstack-devtools/effect": ["effect@3.19.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg=="], + "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], "fumadocs-core/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], @@ -4878,8 +4760,6 @@ "istanbul-reports/html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], - "jsdom/undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], - "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -4892,7 +4772,7 @@ "mdast-util-to-markdown/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "miniflare/undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], "miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], @@ -4904,6 +4784,8 @@ "msw/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "nitro/undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], + "node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -4914,9 +4796,11 @@ "parse5-parser-stream/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - "pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + "pg/pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="], - "posthog-js/@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="], + "pg/pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], + + "pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], "promise-retry/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], @@ -4932,6 +4816,8 @@ "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "rivetkit/uuid": ["uuid@12.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-USe1zesMYh4fjCA8ZH5+X5WIVD0J4V1Jksm1bFTVBX2F/cwSXt0RO5w/3UXbdLKmZX65MiWV+hwhSS8p6oBTGA=="], + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.9", "", {}, "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw=="], "rolldown-plugin-dts/@babel/generator": ["@babel/generator@8.0.0-rc.1", "", { "dependencies": { "@babel/parser": "^8.0.0-rc.1", "@babel/types": "^8.0.0-rc.1", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-3ypWOOiC4AYHKr8vYRVtWtWmyvcoItHtVqF8paFax+ydpmUdPsJpLBkBBs5ItmhdrwC3a0ZSqqFAdzls4ODP3w=="], @@ -5234,6 +5120,18 @@ "@cloudflare/vitest-pool-workers/wrangler/workerd": ["workerd@1.20250906.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250906.0", "@cloudflare/workerd-darwin-arm64": "1.20250906.0", "@cloudflare/workerd-linux-64": "1.20250906.0", "@cloudflare/workerd-linux-arm64": "1.20250906.0", "@cloudflare/workerd-windows-64": "1.20250906.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-ryVyEaqXPPsr/AxccRmYZZmDAkfQVjhfRqrNTlEeN8aftBk6Ca1u7/VqmfOayjCXrA+O547TauebU+J3IpvFXw=="], + "@effect/platform/effect/fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + + "@effect/rpc/effect/fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + + "@effect/sql-pg/pg/pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], + + "@effect/sql-pg/pg/pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="], + + "@effect/sql-pg/pg/pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], + + "@effect/sql-pg/pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -5310,10 +5208,6 @@ "@metascraper/helpers/jsdom/whatwg-url": ["whatwg-url@15.1.0", "", { "dependencies": { "tr46": "^6.0.0", "webidl-conversions": "^8.0.0" } }, "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g=="], - "@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="], - - "@opentelemetry/sdk-trace-web/@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="], - "@rollup/plugin-babel/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], "@rollup/plugin-babel/@rollup/pluginutils/@types/estree": ["@types/estree@0.0.39", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="], @@ -5594,6 +5488,8 @@ "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], + "dfx/effect/fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "dockerode/tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], "dockerode/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], @@ -5648,6 +5544,8 @@ "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], + "effect-rpc-tanstack-devtools/effect/fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fumadocs-core/shiki/@shikijs/core": ["@shikijs/core@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA=="], "fumadocs-core/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw=="], @@ -5762,8 +5660,6 @@ "pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "posthog-js/@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], - "rolldown-plugin-dts/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.2", "", {}, "sha512-noLx87RwlBEMrTzncWd/FvTxoJ9+ycHNg0n8yyYydIoDsLZuxknKgWRJUqcrVkNrJ74uGyhWQzQaS3q8xfGAhQ=="], "ssh-remote-port-forward/@types/ssh2/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], @@ -6050,6 +5946,8 @@ "@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250906.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q8Qjfs8jGVILnZL6vUpQ90q/8MTCYaGR3d1LGxZMBqte8Vr7xF3KFHPEy7tFs0j0mMjnqCYzlofmPNY+9ZaDRg=="], + "@cloudflare/vitest-pool-workers/wrangler/@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], @@ -6110,6 +6008,18 @@ "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250906.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q8Qjfs8jGVILnZL6vUpQ90q/8MTCYaGR3d1LGxZMBqte8Vr7xF3KFHPEy7tFs0j0mMjnqCYzlofmPNY+9ZaDRg=="], + "@effect/platform/effect/fast-check/pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + + "@effect/rpc/effect/fast-check/pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + + "@effect/sql-pg/pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "@effect/sql-pg/pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="], + + "@effect/sql-pg/pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "@effect/sql-pg/pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "@grpc/grpc-js/@grpc/proto-loader/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "@grpc/grpc-js/@grpc/proto-loader/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -6366,8 +6276,12 @@ "babel-dead-code-elimination/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "dfx/effect/fast-check/pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "dockerode/tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "effect-rpc-tanstack-devtools/effect/fast-check/pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "istanbul-lib-instrument/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], "istanbul-lib-instrument/@babel/core/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], diff --git a/libs/ai-openrouter/package.json b/libs/ai-openrouter/package.json index a1aed71e1..08eeedfa7 100644 --- a/libs/ai-openrouter/package.json +++ b/libs/ai-openrouter/package.json @@ -12,13 +12,10 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@effect/ai": "catalog:effect", - "@effect/experimental": "catalog:effect", - "@effect/platform": "catalog:effect", + "@effect/ai-openrouter": "catalog:effect", "effect": "catalog:effect" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "typescript": "^5.9.3" } } diff --git a/libs/ai-openrouter/src/OpenRouterLanguageModel.ts b/libs/ai-openrouter/src/OpenRouterLanguageModel.ts index 6703e3ddd..a4fe7862e 100644 --- a/libs/ai-openrouter/src/OpenRouterLanguageModel.ts +++ b/libs/ai-openrouter/src/OpenRouterLanguageModel.ts @@ -1030,7 +1030,7 @@ const makeStreamResponse: ( // Coerce invalid tool call parameters to an empty object const params = yield* Effect.try(() => Tool.unsafeSecureJsonParse(toolCall.params), - ).pipe(Effect.catchAll(() => Effect.succeed({}))) + ).pipe(Effect.catch(() => Effect.succeed({}))) parts.push({ type: "tool-params-end", id: toolCall.id, diff --git a/libs/bot-sdk/examples/simple-echo-bot/index.ts b/libs/bot-sdk/examples/simple-echo-bot/index.ts index 2fe71c8d1..a807b72ad 100644 --- a/libs/bot-sdk/examples/simple-echo-bot/index.ts +++ b/libs/bot-sdk/examples/simple-echo-bot/index.ts @@ -95,7 +95,7 @@ const program = Effect.gen(function* () { // ctx.args.text is typed as string! yield* bot.message.send(ctx.channelId, `Echo: ${ctx.args.text}`).pipe( Effect.tap((msg) => Effect.log(`✅ Sent echo response: ${msg.id}`)), - Effect.catchAll((error) => Effect.logError(`Failed to send echo: ${error}`)), + Effect.catch((error) => Effect.logError(`Failed to send echo: ${error}`)), ) }), ) @@ -106,7 +106,7 @@ const program = Effect.gen(function* () { // ctx.args is typed as {} (empty object) since PingCommand has no args yield* bot.message.send(ctx.channelId, "Pong! 🏓").pipe( Effect.tap(() => Effect.log("✅ Sent pong response")), - Effect.catchAll((error) => Effect.logError(`Failed to send pong: ${error}`)), + Effect.catch((error) => Effect.logError(`Failed to send pong: ${error}`)), ) }), ) @@ -131,7 +131,7 @@ const program = Effect.gen(function* () { // Echo the message back using bot.message.reply yield* bot.message.reply(message, `Echo: ${message.content}`).pipe( Effect.tap((sentMessage) => Effect.log(`✅ Echoed message: ${sentMessage.id}`)), - Effect.catchAll((error) => Effect.logError(`Failed to send echo: ${error}`)), + Effect.catch((error) => Effect.logError(`Failed to send echo: ${error}`)), ) }), ) @@ -142,7 +142,7 @@ const program = Effect.gen(function* () { if (message.content.toLowerCase().includes("hello")) { yield* bot.message.react(message, "👋").pipe( Effect.tap(() => Effect.log("👋 Waved at hello message")), - Effect.catchAll((error) => Effect.logError(`Failed to react: ${error}`)), + Effect.catch((error) => Effect.logError(`Failed to react: ${error}`)), ) } }), diff --git a/libs/bot-sdk/package.json b/libs/bot-sdk/package.json index a85161188..a89ddb4e5 100644 --- a/libs/bot-sdk/package.json +++ b/libs/bot-sdk/package.json @@ -50,11 +50,8 @@ "prepublishOnly": "GENERATE_DTS=1 bun run build" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@effect/opentelemetry": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-bun": "catalog:effect", - "@effect/rpc": "catalog:effect", "@hazel/actors": "workspace:*", "@hazel/domain": "workspace:*", "@hazel/effect-bun": "workspace:*", @@ -68,12 +65,9 @@ "typescript": "^5.9.3" }, "peerDependencies": { - "@effect/opentelemetry": ">=0.61.0", - "@effect/platform": ">=0.94.0", - "@effect/platform-bun": ">=0.87.0", - "@effect/rpc": ">=0.73.0", - "@effect/workflow": ">=0.16.0", - "effect": ">=3.19.0", + "@effect/opentelemetry": ">=4.0.0-beta.0", + "@effect/platform-bun": ">=4.0.0-beta.0", + "effect": ">=4.0.0-beta.0", "jose": ">=6.0.0", "rivetkit": ">=2.0.0" }, @@ -86,9 +80,6 @@ }, "rivetkit": { "optional": true - }, - "@effect/workflow": { - "optional": true } }, "engines": { diff --git a/libs/bot-sdk/src/auth.ts b/libs/bot-sdk/src/auth.ts index 6578e515d..3e371de2d 100644 --- a/libs/bot-sdk/src/auth.ts +++ b/libs/bot-sdk/src/auth.ts @@ -1,6 +1,7 @@ -import { FetchHttpClient, HttpApiClient, HttpClient, HttpClientRequest } from "@effect/platform" +import { HttpApiClient } from "effect/unstable/httpapi" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" import { HazelApi } from "@hazel/domain/http" -import { Duration, Effect, Schedule } from "effect" +import { ServiceMap, Duration, Effect, Schedule } from "effect" import { AuthenticationError } from "./errors.ts" /** @@ -36,9 +37,8 @@ export interface BotAuthContext { /** * Service for bot authentication */ -export class BotAuth extends Effect.Service()("BotAuth", { - accessors: true, - effect: Effect.fn(function* (context: BotAuthContext) { +export class BotAuth extends ServiceMap.Service()("BotAuth", { + make: Effect.fn(function* (context: BotAuthContext) { return { getContext: Effect.succeed(context), diff --git a/libs/bot-sdk/src/errors.ts b/libs/bot-sdk/src/errors.ts index 243fab60a..d2067cf73 100644 --- a/libs/bot-sdk/src/errors.ts +++ b/libs/bot-sdk/src/errors.ts @@ -3,7 +3,7 @@ import { Schema } from "effect" /** * Error thrown when bot authentication fails. */ -export class AuthenticationError extends Schema.TaggedError()("AuthenticationError", { +export class AuthenticationError extends Schema.TaggedErrorClass()("AuthenticationError", { message: Schema.String, cause: Schema.Unknown, }) {} @@ -11,7 +11,7 @@ export class AuthenticationError extends Schema.TaggedError /** * Error thrown when a command payload cannot be decoded. */ -export class CommandArgsDecodeError extends Schema.TaggedError()( +export class CommandArgsDecodeError extends Schema.TaggedErrorClass()( "CommandArgsDecodeError", { message: Schema.String, @@ -23,7 +23,7 @@ export class CommandArgsDecodeError extends Schema.TaggedError()("CommandHandlerError", { +export class CommandHandlerError extends Schema.TaggedErrorClass()("CommandHandlerError", { message: Schema.String, commandName: Schema.String, cause: Schema.Unknown, @@ -32,7 +32,7 @@ export class CommandHandlerError extends Schema.TaggedError /** * Error thrown when syncing slash commands with the backend fails. */ -export class CommandSyncError extends Schema.TaggedError()("CommandSyncError", { +export class CommandSyncError extends Schema.TaggedErrorClass()("CommandSyncError", { message: Schema.String, cause: Schema.Unknown, }) {} @@ -40,23 +40,23 @@ export class CommandSyncError extends Schema.TaggedError()("Co /** * Error thrown when syncing mentionable settings fails. */ -export class MentionableSyncError extends Schema.TaggedError()("MentionableSyncError", { +export class MentionableSyncError extends Schema.TaggedErrorClass()("MentionableSyncError", { message: Schema.String, cause: Schema.Unknown, }) {} -export class GatewayReadError extends Schema.TaggedError()("GatewayReadError", { +export class GatewayReadError extends Schema.TaggedErrorClass()("GatewayReadError", { message: Schema.String, cause: Schema.Unknown, }) {} -export class GatewayDecodeError extends Schema.TaggedError()("GatewayDecodeError", { +export class GatewayDecodeError extends Schema.TaggedErrorClass()("GatewayDecodeError", { message: Schema.String, payload: Schema.String, cause: Schema.Unknown, }) {} -export class GatewaySessionStoreError extends Schema.TaggedError()( +export class GatewaySessionStoreError extends Schema.TaggedErrorClass()( "GatewaySessionStoreError", { message: Schema.String, @@ -67,7 +67,7 @@ export class GatewaySessionStoreError extends Schema.TaggedError()("MessageSendError", { +export class MessageSendError extends Schema.TaggedErrorClass()("MessageSendError", { message: Schema.String, channelId: Schema.String, cause: Schema.Unknown, @@ -76,7 +76,7 @@ export class MessageSendError extends Schema.TaggedError()("Me /** * Error thrown when replying to a message fails. */ -export class MessageReplyError extends Schema.TaggedError()("MessageReplyError", { +export class MessageReplyError extends Schema.TaggedErrorClass()("MessageReplyError", { message: Schema.String, channelId: Schema.String, replyToMessageId: Schema.String, @@ -86,7 +86,7 @@ export class MessageReplyError extends Schema.TaggedError()(" /** * Error thrown when updating a message fails. */ -export class MessageUpdateError extends Schema.TaggedError()("MessageUpdateError", { +export class MessageUpdateError extends Schema.TaggedErrorClass()("MessageUpdateError", { message: Schema.String, messageId: Schema.String, cause: Schema.Unknown, @@ -95,7 +95,7 @@ export class MessageUpdateError extends Schema.TaggedError() /** * Error thrown when deleting a message fails. */ -export class MessageDeleteError extends Schema.TaggedError()("MessageDeleteError", { +export class MessageDeleteError extends Schema.TaggedErrorClass()("MessageDeleteError", { message: Schema.String, messageId: Schema.String, cause: Schema.Unknown, @@ -104,7 +104,7 @@ export class MessageDeleteError extends Schema.TaggedError() /** * Error thrown when toggling a reaction fails. */ -export class MessageReactError extends Schema.TaggedError()("MessageReactError", { +export class MessageReactError extends Schema.TaggedErrorClass()("MessageReactError", { message: Schema.String, messageId: Schema.String, emoji: Schema.String, @@ -114,7 +114,7 @@ export class MessageReactError extends Schema.TaggedError()(" /** * Error thrown when listing messages fails. */ -export class MessageListError extends Schema.TaggedError()("MessageListError", { +export class MessageListError extends Schema.TaggedErrorClass()("MessageListError", { message: Schema.String, channelId: Schema.String, cause: Schema.Unknown, @@ -123,7 +123,7 @@ export class MessageListError extends Schema.TaggedError()("Me /** * Error thrown when an event handler execution fails. */ -export class EventHandlerError extends Schema.TaggedError()("EventHandlerError", { +export class EventHandlerError extends Schema.TaggedErrorClass()("EventHandlerError", { message: Schema.String, eventType: Schema.String, cause: Schema.Unknown, diff --git a/libs/bot-sdk/src/hazel-bot-sdk.ts b/libs/bot-sdk/src/hazel-bot-sdk.ts index a9631c08e..52db6b616 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.ts @@ -5,7 +5,8 @@ * Hazel message, channel, membership, and command events are pre-configured. */ -import { FetchHttpClient, HttpApiClient } from "@effect/platform" +import { HttpApiClient } from "effect/unstable/httpapi" +import { FetchHttpClient } from "effect/unstable/http" import type { AttachmentId, BotId, @@ -184,9 +185,8 @@ export interface SendMessageOptions { * Hazel Bot Client - Effect Service with typed convenience methods * Uses scoped: since it manages scoped resources (RateLimiter) */ -export class HazelBotClient extends Effect.Service()("HazelBotClient", { - accessors: true, - scoped: Effect.gen(function* () { +export class HazelBotClient extends ServiceMap.Service()("HazelBotClient", { + make: Effect.gen(function* () { const auth = yield* BotAuth // Get the RPC client from context const rpc = yield* BotRpcClient @@ -807,7 +807,7 @@ export class HazelBotClient extends Effect.Service()("HazelBotCl Effect.forever( Effect.gen(function* () { const nextState = yield* connectOnce.pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.logWarning("Bot gateway websocket failed, retrying", { error, botId: authContext.botId, @@ -1751,13 +1751,13 @@ export class HazelBotClient extends Effect.Service()("HazelBotCl // Mark the session as failed - this updates the existing message yield* session.fail(userMessage).pipe( - Effect.catchAllCause((cause) => + Effect.catchCause((cause) => Effect.gen(function* () { yield* Effect.logError("Failed to mark AI stream as failed", { cause, }) yield* sendCommandErrorMessage(ctx, userMessage).pipe( - Effect.catchAllCause((messageCause) => + Effect.catchCause((messageCause) => Effect.logError( "Failed to send AI fallback error message", { @@ -1911,7 +1911,7 @@ export interface HazelBotConfig = EmptyComman * @example * ```typescript * import { createHazelBot, HazelBotClient, Command, CommandGroup } from "@hazel/bot-sdk" - * import { Schema } from "effect" + * import { ServiceMap, Schema } from "effect" * * // Define typesafe commands * const EchoCommand = Command.make("echo", { diff --git a/libs/bot-sdk/src/log-context.ts b/libs/bot-sdk/src/log-context.ts index bbe94844f..4b786b806 100644 --- a/libs/bot-sdk/src/log-context.ts +++ b/libs/bot-sdk/src/log-context.ts @@ -203,7 +203,7 @@ const contextToSpanAttributes = (ctx: LogContext): Record => { export const withLogContext = ( ctx: LogContext, spanName: string, - effect: Effect.Effect, + make: Effect.Effect, options?: { readonly parent?: Tracer.AnySpan }, ): Effect.Effect => effect.pipe( diff --git a/libs/bot-sdk/src/rpc/auth-middleware.ts b/libs/bot-sdk/src/rpc/auth-middleware.ts index fa27b0c6b..a34a99112 100644 --- a/libs/bot-sdk/src/rpc/auth-middleware.ts +++ b/libs/bot-sdk/src/rpc/auth-middleware.ts @@ -5,8 +5,8 @@ * into all RPC requests for authentication with the backend. */ -import { Headers } from "@effect/platform" -import { RpcMiddleware } from "@effect/rpc" +import { Headers } from "effect/unstable/http" +import { RpcMiddleware } from "effect/unstable/rpc" import { AuthMiddleware } from "@hazel/domain/rpc" import { Effect } from "effect" diff --git a/libs/bot-sdk/src/rpc/client.ts b/libs/bot-sdk/src/rpc/client.ts index be265281a..cfc938169 100644 --- a/libs/bot-sdk/src/rpc/client.ts +++ b/libs/bot-sdk/src/rpc/client.ts @@ -5,10 +5,10 @@ * Uses FetchHttpClient for HTTP transport and NDJSON serialization. */ -import { FetchHttpClient } from "@effect/platform" -import { RpcClient, RpcSerialization } from "@effect/rpc" +import { FetchHttpClient } from "effect/unstable/http" +import { RpcClient, RpcSerialization } from "effect/unstable/rpc" import { ChannelRpcs, MessageReactionRpcs, MessageRpcs, TypingIndicatorRpcs } from "@hazel/domain/rpc" -import { Context, Effect, Layer } from "effect" +import { Effect, Layer, ServiceMap } from "effect" import { createBotAuthMiddleware } from "./auth-middleware.ts" /** @@ -36,19 +36,17 @@ export interface BotRpcClientConfig { /** * Internal context tag for the RPC client configuration */ -export class BotRpcClientConfigTag extends Context.Tag("@hazel/bot-sdk/BotRpcClientConfig")< - BotRpcClientConfigTag, +export class BotRpcClientConfigTag extends ServiceMap.Service() {} +>()("@hazel/bot-sdk/BotRpcClientConfig") {} /** * Context tag for the RPC client instance * Type is inferred from the actual RpcClient.make result */ -export class BotRpcClient extends Context.Tag("@hazel/bot-sdk/BotRpcClient")< - BotRpcClient, +export class BotRpcClient extends ServiceMap.Service> ->() {} +>()("@hazel/bot-sdk/BotRpcClient") {} /** * Create a scoped layer that provides the RPC client diff --git a/libs/bot-sdk/src/services/health-server.ts b/libs/bot-sdk/src/services/health-server.ts index 3782c50ba..96db22b44 100644 --- a/libs/bot-sdk/src/services/health-server.ts +++ b/libs/bot-sdk/src/services/health-server.ts @@ -7,16 +7,15 @@ * Enabled by default on port 9090. Set `healthPort: false` in config to disable. */ -import { Context, Effect, Layer, Runtime } from "effect" +import { Effect, Layer, Runtime, ServiceMap } from "effect" export interface BotHealthServerConfig { readonly port: number } -export class BotHealthServerConfigTag extends Context.Tag("@hazel/bot-sdk/BotHealthServerConfig")< - BotHealthServerConfigTag, +export class BotHealthServerConfigTag extends ServiceMap.Service() {} +>()("@hazel/bot-sdk/BotHealthServerConfig") {} interface HealthResponse { readonly status: "healthy" @@ -24,9 +23,8 @@ interface HealthResponse { readonly uptime_ms: number } -export class BotHealthServer extends Effect.Service()("BotHealthServer", { - accessors: true, - scoped: Effect.gen(function* () { +export class BotHealthServer extends ServiceMap.Service()("BotHealthServer", { + make: Effect.gen(function* () { const config = yield* BotHealthServerConfigTag const startTime = Date.now() const runtime = yield* Effect.runtime() diff --git a/libs/bot-sdk/src/streaming/actors-client.ts b/libs/bot-sdk/src/streaming/actors-client.ts index 63c5d1220..8249f7c9f 100644 --- a/libs/bot-sdk/src/streaming/actors-client.ts +++ b/libs/bot-sdk/src/streaming/actors-client.ts @@ -8,7 +8,7 @@ */ import { createActorsClient } from "@hazel/actors/client" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" // biome-ignore lint/suspicious/noExplicitAny: Opaque type to avoid non-portable DTS references to @hazel/actors internals export type MessageActor = any @@ -72,9 +72,8 @@ export interface ActorsClientConfig { * }).pipe(Effect.provide(layer)) * ``` */ -export class ActorsClient extends Effect.Service()("@hazel/bot-sdk/ActorsClient", { - accessors: true, - effect: Effect.fn("ActorsClient.create")(function* (config: ActorsClientConfig) { +export class ActorsClient extends ServiceMap.Service()("@hazel/bot-sdk/ActorsClient", { + make: Effect.fn("ActorsClient.create")(function* (config: ActorsClientConfig) { const endpoint = config.endpoint ?? "https://rivet.hazel.sh" const client = createActorsClient(endpoint) diff --git a/libs/bot-sdk/src/streaming/errors.ts b/libs/bot-sdk/src/streaming/errors.ts index 2f14b6ef4..599f6baec 100644 --- a/libs/bot-sdk/src/streaming/errors.ts +++ b/libs/bot-sdk/src/streaming/errors.ts @@ -10,7 +10,7 @@ import { Schema } from "effect" /** * Error thrown when connecting to a message actor fails */ -export class ActorConnectionError extends Schema.TaggedError()("ActorConnectionError", { +export class ActorConnectionError extends Schema.TaggedErrorClass()("ActorConnectionError", { messageId: Schema.String, message: Schema.String, cause: Schema.Unknown, @@ -19,7 +19,7 @@ export class ActorConnectionError extends Schema.TaggedError()("MessageCreateError", { +export class MessageCreateError extends Schema.TaggedErrorClass()("MessageCreateError", { channelId: Schema.String, message: Schema.String, cause: Schema.Unknown, @@ -28,7 +28,7 @@ export class MessageCreateError extends Schema.TaggedError() /** * Error thrown when an actor operation (appendText, complete, etc.) fails */ -export class ActorOperationError extends Schema.TaggedError()("ActorOperationError", { +export class ActorOperationError extends Schema.TaggedErrorClass()("ActorOperationError", { operation: Schema.String, message: Schema.String, cause: Schema.Unknown, @@ -37,7 +37,7 @@ export class ActorOperationError extends Schema.TaggedError /** * Error thrown when processing an async stream of chunks fails */ -export class StreamProcessingError extends Schema.TaggedError()( +export class StreamProcessingError extends Schema.TaggedErrorClass()( "StreamProcessingError", { message: Schema.String, @@ -48,7 +48,7 @@ export class StreamProcessingError extends Schema.TaggedError()( +export class BotNotConfiguredError extends Schema.TaggedErrorClass()( "BotNotConfiguredError", { message: Schema.String, @@ -60,7 +60,7 @@ export class BotNotConfiguredError extends Schema.TaggedError()("MessagePersistError", { +export class MessagePersistError extends Schema.TaggedErrorClass()("MessagePersistError", { messageId: Schema.String, message: Schema.String, cause: Schema.Unknown, diff --git a/libs/bot-sdk/src/streaming/streaming-service.ts b/libs/bot-sdk/src/streaming/streaming-service.ts index 56428d4c6..8e45f9caf 100644 --- a/libs/bot-sdk/src/streaming/streaming-service.ts +++ b/libs/bot-sdk/src/streaming/streaming-service.ts @@ -161,7 +161,7 @@ const createSessionFromActor = ( embeds: [{ liveState: { enabled: true, cached } }], }) .pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.logWarning("Failed to persist streaming message to database", { messageId, error: String(error), @@ -199,7 +199,7 @@ const createSessionFromActor = ( embeds: [{ liveState: { enabled: true, cached } }], }) .pipe( - Effect.catchAll((persistError) => + Effect.catch((persistError) => Effect.logWarning("Failed to persist failed streaming state to database", { messageId, error: String(persistError), diff --git a/libs/effect-electric-db-collection/package.json b/libs/effect-electric-db-collection/package.json index c0ad024e8..2983a439b 100644 --- a/libs/effect-electric-db-collection/package.json +++ b/libs/effect-electric-db-collection/package.json @@ -12,7 +12,6 @@ "test": "npx vitest --run" }, "dependencies": { - "@effect-atom/atom": "catalog:effect", "@electric-sql/client": "1.5.12", "@standard-schema/spec": "^1.1.0", "@tanstack/db": "0.5.31", @@ -22,7 +21,6 @@ "effect": "catalog:effect" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@vitest/coverage-istanbul": "^4.0.17", "typescript": "^5.9.3" } diff --git a/libs/effect-electric-db-collection/src/collection.ts b/libs/effect-electric-db-collection/src/collection.ts index b906ff871..e732f3229 100644 --- a/libs/effect-electric-db-collection/src/collection.ts +++ b/libs/effect-electric-db-collection/src/collection.ts @@ -17,7 +17,7 @@ export type { CollectionStatus } from "@tanstack/db" * Error returned when the collection's last error is retrieved. * Wraps the underlying TanStack DB error with collection context. */ -export class CollectionSyncEffectError extends Schema.TaggedError()( +export class CollectionSyncEffectError extends Schema.TaggedErrorClass()( "CollectionSyncEffectError", { message: Schema.String, diff --git a/libs/effect-electric-db-collection/src/errors.ts b/libs/effect-electric-db-collection/src/errors.ts index f9ea9c760..ca5f8efa5 100644 --- a/libs/effect-electric-db-collection/src/errors.ts +++ b/libs/effect-electric-db-collection/src/errors.ts @@ -3,7 +3,7 @@ import { Schema } from "effect" /** * Base error for Electric Collection operations */ -export class ElectricCollectionError extends Schema.TaggedError()( +export class ElectricCollectionError extends Schema.TaggedErrorClass()( "ElectricCollectionError", { message: Schema.String, @@ -14,7 +14,7 @@ export class ElectricCollectionError extends Schema.TaggedError()("InsertError", { +export class InsertError extends Schema.TaggedErrorClass()("InsertError", { message: Schema.String, data: Schema.optional(Schema.Unknown), cause: Schema.optional(Schema.Unknown), @@ -23,7 +23,7 @@ export class InsertError extends Schema.TaggedError()("InsertError" /** * Error thrown when an update operation fails */ -export class UpdateError extends Schema.TaggedError()("UpdateError", { +export class UpdateError extends Schema.TaggedErrorClass()("UpdateError", { message: Schema.String, key: Schema.optional(Schema.Unknown), cause: Schema.optional(Schema.Unknown), @@ -32,7 +32,7 @@ export class UpdateError extends Schema.TaggedError()("UpdateError" /** * Error thrown when a delete operation fails */ -export class DeleteError extends Schema.TaggedError()("DeleteError", { +export class DeleteError extends Schema.TaggedErrorClass()("DeleteError", { message: Schema.String, key: Schema.optional(Schema.Unknown), cause: Schema.optional(Schema.Unknown), @@ -41,7 +41,7 @@ export class DeleteError extends Schema.TaggedError()("DeleteError" /** * Error thrown when waiting for a transaction ID times out */ -export class TxIdTimeoutError extends Schema.TaggedError()("TxIdTimeoutError", { +export class TxIdTimeoutError extends Schema.TaggedErrorClass()("TxIdTimeoutError", { message: Schema.String, txid: Schema.Number, timeout: Schema.Number, @@ -50,7 +50,7 @@ export class TxIdTimeoutError extends Schema.TaggedError()("Tx /** * Error thrown when a required transaction ID is missing from handler result */ -export class MissingTxIdError extends Schema.TaggedError()("MissingTxIdError", { +export class MissingTxIdError extends Schema.TaggedErrorClass()("MissingTxIdError", { message: Schema.String, operation: Schema.Literal("insert", "update", "delete"), }) {} @@ -58,7 +58,7 @@ export class MissingTxIdError extends Schema.TaggedError()("Mi /** * Error thrown when an invalid transaction ID type is provided */ -export class InvalidTxIdError extends Schema.TaggedError()("InvalidTxIdError", { +export class InvalidTxIdError extends Schema.TaggedErrorClass()("InvalidTxIdError", { message: Schema.String, receivedType: Schema.String, }) {} @@ -66,7 +66,7 @@ export class InvalidTxIdError extends Schema.TaggedError()("In /** * Error thrown when sync configuration is invalid */ -export class SyncConfigError extends Schema.TaggedError()("SyncConfigError", { +export class SyncConfigError extends Schema.TaggedErrorClass()("SyncConfigError", { message: Schema.String, cause: Schema.optional(Schema.Unknown), }) {} @@ -74,7 +74,7 @@ export class SyncConfigError extends Schema.TaggedError()("Sync /** * Error thrown when an optimistic action fails */ -export class OptimisticActionError extends Schema.TaggedError()( +export class OptimisticActionError extends Schema.TaggedErrorClass()( "OptimisticActionError", { message: Schema.String, @@ -85,7 +85,7 @@ export class OptimisticActionError extends Schema.TaggedError()("SyncError", { +export class SyncError extends Schema.TaggedErrorClass()("SyncError", { message: Schema.String, txid: Schema.optional(Schema.Number), collectionName: Schema.optional(Schema.String), diff --git a/libs/effect-electric-db-collection/src/handlers.ts b/libs/effect-electric-db-collection/src/handlers.ts index 8a762d2f8..d992ad7c0 100644 --- a/libs/effect-electric-db-collection/src/handlers.ts +++ b/libs/effect-electric-db-collection/src/handlers.ts @@ -32,7 +32,7 @@ export function convertInsertHandler< return async (params: InsertMutationFnParams) => { const effect = handler(params).pipe( - Effect.catchAll((error: E | unknown) => + Effect.catch((error: E | unknown) => Effect.fail( new InsertError({ message: `Insert operation failed`, @@ -97,7 +97,7 @@ export function convertUpdateHandler< return async (params: UpdateMutationFnParams) => { const effect = handler(params).pipe( - Effect.catchAll((error: E | unknown) => + Effect.catch((error: E | unknown) => Effect.fail( new UpdateError({ message: `Update operation failed`, @@ -162,7 +162,7 @@ export function convertDeleteHandler< return async (params: DeleteMutationFnParams) => { const effect = handler(params).pipe( - Effect.catchAll((error: E | unknown) => + Effect.catch((error: E | unknown) => Effect.fail( new DeleteError({ message: `Delete operation failed`, diff --git a/libs/effect-electric-db-collection/src/optimistic-action.ts b/libs/effect-electric-db-collection/src/optimistic-action.ts index 7728d8d3e..12f1f265d 100644 --- a/libs/effect-electric-db-collection/src/optimistic-action.ts +++ b/libs/effect-electric-db-collection/src/optimistic-action.ts @@ -1,4 +1,4 @@ -import { Atom, type Result } from "@effect-atom/atom" +import { Atom, type Result } from "effect" import type { Collection, Transaction } from "@tanstack/db" import { createTransaction } from "@tanstack/db" import type { Txid } from "@tanstack/electric-db-collection" diff --git a/libs/effect-electric-db-collection/src/tanstack-errors.ts b/libs/effect-electric-db-collection/src/tanstack-errors.ts index 3d839ee8c..ff0667d32 100644 --- a/libs/effect-electric-db-collection/src/tanstack-errors.ts +++ b/libs/effect-electric-db-collection/src/tanstack-errors.ts @@ -21,7 +21,7 @@ export type ValidationIssue = typeof ValidationIssue.Type * * @permanent This error will not resolve on retry - the data must be modified */ -export class DuplicateKeyEffectError extends Schema.TaggedError()( +export class DuplicateKeyEffectError extends Schema.TaggedErrorClass()( "DuplicateKeyEffectError", { message: Schema.String, @@ -36,7 +36,7 @@ export class DuplicateKeyEffectError extends Schema.TaggedError()( +export class KeyUpdateNotAllowedEffectError extends Schema.TaggedErrorClass()( "KeyUpdateNotAllowedEffectError", { message: Schema.String, @@ -51,7 +51,7 @@ export class KeyUpdateNotAllowedEffectError extends Schema.TaggedError()( +export class UndefinedKeyEffectError extends Schema.TaggedErrorClass()( "UndefinedKeyEffectError", { message: Schema.String, @@ -65,7 +65,7 @@ export class UndefinedKeyEffectError extends Schema.TaggedError()( +export class SchemaValidationEffectError extends Schema.TaggedErrorClass()( "SchemaValidationEffectError", { message: Schema.String, @@ -86,7 +86,7 @@ export class SchemaValidationEffectError extends Schema.TaggedError()( +export class KeyNotFoundEffectError extends Schema.TaggedErrorClass()( "KeyNotFoundEffectError", { message: Schema.String, @@ -101,7 +101,7 @@ export class KeyNotFoundEffectError extends Schema.TaggedError()( +export class CollectionInErrorEffectError extends Schema.TaggedErrorClass()( "CollectionInErrorEffectError", { message: Schema.String, @@ -116,7 +116,7 @@ export class CollectionInErrorEffectError extends Schema.TaggedError()( +export class TransactionStateEffectError extends Schema.TaggedErrorClass()( "TransactionStateEffectError", { message: Schema.String, diff --git a/libs/tanstack-db-atom/package.json b/libs/tanstack-db-atom/package.json index efc4f0e64..702329ecb 100644 --- a/libs/tanstack-db-atom/package.json +++ b/libs/tanstack-db-atom/package.json @@ -8,12 +8,11 @@ ".": "./src/index.ts" }, "dependencies": { - "@effect-atom/atom-react": "catalog:effect", + "@effect/atom-react": "catalog:effect", "@tanstack/db": "0.5.31", "effect": "catalog:effect" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "typescript": "^5.9.3" } } diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.result.test.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.result.test.ts index 4ee0bd52f..10d33ac7f 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.result.test.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.result.test.ts @@ -9,7 +9,7 @@ * @since 1.0.0 */ -import { Atom, Registry, Result } from "@effect-atom/atom-react" +import { Atom, Registry, Result } from "@effect/atom-react" import { type Collection, createCollection, eq, type NonSingleResult } from "@tanstack/db" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { makeCollectionAtom, makeQuery, makeQueryConditional, makeQueryUnsafe } from "./AtomTanStackDB" diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.subscription.test.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.subscription.test.ts index 9773a80f2..1343b30ba 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.subscription.test.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.subscription.test.ts @@ -9,7 +9,7 @@ * @since 1.0.0 */ -import { Atom, Registry, Result } from "@effect-atom/atom-react" +import { Atom, Registry, Result } from "@effect/atom-react" import { type Collection, createCollection, eq, type NonSingleResult } from "@tanstack/db" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { makeCollectionAtom, makeQuery } from "./AtomTanStackDB" diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.test.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.test.ts index 7e784fa2c..e7f143d1d 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.test.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.test.ts @@ -16,7 +16,7 @@ * @since 1.0.0 */ -import { Registry, Result } from "@effect-atom/atom-react" +import { Registry, Result } from "@effect/atom-react" import { type Collection, createCollection, eq, type NonSingleResult, type SingleResult } from "@tanstack/db" import { describe, expect, it } from "vitest" import { diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.timing.test.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.timing.test.ts index 4ec4032cb..432bad1b7 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.timing.test.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.timing.test.ts @@ -9,7 +9,7 @@ * @since 1.0.0 */ -import { Atom, Registry, Result } from "@effect-atom/atom-react" +import { Atom, Registry, Result } from "@effect/atom-react" import { type Collection, createCollection, eq, type NonSingleResult } from "@tanstack/db" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { makeCollectionAtom, makeQuery } from "./AtomTanStackDB" diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.ts index 7e734ec2a..df5e510fa 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.ts @@ -4,7 +4,7 @@ * @since 1.0.0 */ -import { Atom, Result } from "@effect-atom/atom-react" +import { Atom, Result } from "@effect/atom-react" import { type Collection, type Context, diff --git a/libs/tanstack-db-atom/src/types.ts b/libs/tanstack-db-atom/src/types.ts index be1120132..ce0e6661b 100644 --- a/libs/tanstack-db-atom/src/types.ts +++ b/libs/tanstack-db-atom/src/types.ts @@ -3,7 +3,7 @@ * @since 1.0.0 */ -import type { Atom, Result } from "@effect-atom/atom-react" +import type { Atom, Result } from "@effect/atom-react" import type { Collection, Context, diff --git a/package.json b/package.json index 5e13c7ff1..7b7acdee9 100644 --- a/package.json +++ b/package.json @@ -10,25 +10,15 @@ ], "catalogs": { "effect": { - "effect": "3.19.19", - "@effect/platform": "0.94.5", - "@effect/platform-bun": "0.87.1", - "@effect/platform-browser": "0.74.0", - "@effect/platform-node": "0.104.1", - "@effect/rpc": "0.73.2", - "@effect/experimental": "0.58.0", - "@effect/language-service": "0.79.0", - "@effect/sql": "0.49.0", - "@effect/sql-pg": "0.50.3", - "@effect/cluster": "0.56.4", - "@effect/opentelemetry": "0.61.0", - "@effect/workflow": "0.16.0", - "@effect-atom/atom": "0.5.3", - "@effect-atom/atom-react": "0.5.0", - "@effect/ai": "0.33.2", - "@effect/cli": "0.73.2", - "@effect/printer": "0.47.0", - "@effect/printer-ansi": "0.47.0" + "effect": "4.0.0-beta.32", + "@effect/platform-bun": "4.0.0-beta.32", + "@effect/platform-browser": "4.0.0-beta.32", + "@effect/platform-node": "4.0.0-beta.32", + "@effect/sql-pg": "4.0.0-beta.32", + "@effect/opentelemetry": "4.0.0-beta.32", + "@effect/atom-react": "4.0.0-beta.32", + "@effect/ai-openrouter": "4.0.0-beta.32", + "@effect/vitest": "4.0.0-beta.32" } } }, @@ -49,7 +39,7 @@ "test:coverage": "vitest run --coverage --coverage.reporter=text" }, "devDependencies": { - "@effect/vitest": "^0.27.0", + "@effect/vitest": "catalog:effect", "@rolldown/plugin-babel": "^0.2.1", "@vitest/coverage-v8": "^4.1.0", "oxfmt": "^0.40.0", diff --git a/packages/actors/package.json b/packages/actors/package.json index 884760668..72153ee47 100644 --- a/packages/actors/package.json +++ b/packages/actors/package.json @@ -13,7 +13,6 @@ "test": "vitest run" }, "dependencies": { - "@effect/platform": "catalog:effect", "@hazel/rivet-effect": "workspace:*", "@hazel/schema": "workspace:*", "effect": "catalog:effect", diff --git a/packages/actors/src/auth/config-service.ts b/packages/actors/src/auth/config-service.ts index 53943152b..832fb6eed 100644 --- a/packages/actors/src/auth/config-service.ts +++ b/packages/actors/src/auth/config-service.ts @@ -1,5 +1,5 @@ import type { WorkOSClientId } from "@hazel/schema" -import { Config, Effect, Option, Redacted, Schema } from "effect" +import { ServiceMap, Config, Effect, Option, Redacted, Schema } from "effect" import { WorkOSClientId as WorkOSClientIdSchema } from "@hazel/schema" /** @@ -21,10 +21,9 @@ const optionalValue = (effect: Effect.Effect) => effect.pipe(E * * Uses Effect.Config to load from environment variables with proper fallbacks. */ -export class TokenValidationConfigService extends Effect.Service()( +export class TokenValidationConfigService extends ServiceMap.Service()( "TokenValidationConfigService", { - accessors: true, effect: Effect.gen(function* () { const workosClientId = yield* optionalValue( Config.string("WORKOS_CLIENT_ID").pipe( diff --git a/packages/actors/src/auth/errors.ts b/packages/actors/src/auth/errors.ts index ee9fb4efd..c3971947b 100644 --- a/packages/actors/src/auth/errors.ts +++ b/packages/actors/src/auth/errors.ts @@ -3,14 +3,14 @@ import { Schema } from "effect" /** * Error when loading configuration from environment */ -export class ConfigError extends Schema.TaggedError()("ConfigError", { +export class ConfigError extends Schema.TaggedErrorClass()("ConfigError", { message: Schema.String, }) {} /** * Error when token format is invalid (not a JWT or bot token) */ -export class InvalidTokenFormatError extends Schema.TaggedError()( +export class InvalidTokenFormatError extends Schema.TaggedErrorClass()( "InvalidTokenFormatError", { message: Schema.String, @@ -20,7 +20,7 @@ export class InvalidTokenFormatError extends Schema.TaggedError()("JwtValidationError", { +export class JwtValidationError extends Schema.TaggedErrorClass()("JwtValidationError", { message: Schema.String, cause: Schema.optional(Schema.Unknown), }) {} @@ -28,7 +28,7 @@ export class JwtValidationError extends Schema.TaggedError() /** * Error when bot token validation fails (invalid token, backend error, etc.) */ -export class BotTokenValidationError extends Schema.TaggedError()( +export class BotTokenValidationError extends Schema.TaggedErrorClass()( "BotTokenValidationError", { message: Schema.String, diff --git a/packages/actors/src/auth/jwks-service.ts b/packages/actors/src/auth/jwks-service.ts index a3249e9d3..b51681148 100644 --- a/packages/actors/src/auth/jwks-service.ts +++ b/packages/actors/src/auth/jwks-service.ts @@ -1,4 +1,4 @@ -import { Effect, Option, Ref } from "effect" +import { ServiceMap, Effect, Option, Ref } from "effect" import { createRemoteJWKSet, type JWTVerifyGetKey } from "jose" import { TokenValidationConfigService } from "./config-service" import { ConfigError } from "./errors" @@ -8,10 +8,9 @@ import { ConfigError } from "./errors" * * The keyset is created lazily the first time JWT validation is requested. */ -export class JwksService extends Effect.Service()("JwksService", { - accessors: true, +export class JwksService extends ServiceMap.Service()("JwksService", { dependencies: [TokenValidationConfigService.Default], - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const config = yield* TokenValidationConfigService const jwksRef = yield* Ref.make>(Option.none()) diff --git a/packages/actors/src/auth/token-validation-service.ts b/packages/actors/src/auth/token-validation-service.ts index 30207f91b..f3962ef70 100644 --- a/packages/actors/src/auth/token-validation-service.ts +++ b/packages/actors/src/auth/token-validation-service.ts @@ -1,6 +1,6 @@ -import { HttpClient, HttpClientRequest } from "@effect/platform" +import { HttpClient, HttpClientRequest } from "effect/unstable/http" import { WorkOSJwtClaims, WorkOSRole } from "@hazel/schema" -import { Either, Effect, Option, Redacted, Schema } from "effect" +import { ServiceMap, Either, Effect, Option, Redacted, Schema } from "effect" import { TreeFormatter } from "effect/ParseResult" import type { JWTPayload } from "jose" import { jwtVerify } from "jose" @@ -39,10 +39,9 @@ function isBotToken(token: string): boolean { * * Provides Effect-native token validation with proper error types. */ -export class TokenValidationService extends Effect.Service()( +export class TokenValidationService extends ServiceMap.Service()( "TokenValidationService", { - accessors: true, dependencies: [TokenValidationConfigService.Default, JwksService.Default], effect: Effect.gen(function* () { const config = yield* TokenValidationConfigService @@ -177,7 +176,7 @@ export class TokenValidationService extends Effect.Service= 400) { const errorText = yield* response.text.pipe( - Effect.catchAll(() => Effect.succeed("Unknown error")), + Effect.catch(() => Effect.succeed("Unknown error")), ) return yield* Effect.fail( diff --git a/packages/actors/src/effect/runtime.ts b/packages/actors/src/effect/runtime.ts index b91913db6..a4976f00b 100644 --- a/packages/actors/src/effect/runtime.ts +++ b/packages/actors/src/effect/runtime.ts @@ -1,4 +1,4 @@ -import { FetchHttpClient } from "@effect/platform" +import { FetchHttpClient } from "effect/unstable/http" import { Layer, ManagedRuntime } from "effect" import { TokenValidationLive } from "../auth" diff --git a/packages/auth/package.json b/packages/auth/package.json index 9ceb055a1..e343c8cb6 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -11,8 +11,6 @@ "./errors": "./src/errors.ts" }, "dependencies": { - "@effect/experimental": "catalog:effect", - "@effect/platform": "catalog:effect", "@hazel/db": "workspace:*", "@hazel/domain": "workspace:*", "@hazel/effect-bun": "workspace:*", @@ -22,7 +20,6 @@ "jose": "^6.1.3" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3" } diff --git a/packages/auth/src/cache/user-lookup-cache.ts b/packages/auth/src/cache/user-lookup-cache.ts index 5e138b1b4..21e9503f7 100644 --- a/packages/auth/src/cache/user-lookup-cache.ts +++ b/packages/auth/src/cache/user-lookup-cache.ts @@ -1,6 +1,6 @@ -import { Persistence } from "@effect/experimental" +import { Persistence } from "effect/unstable/persistence" import type { UserId, WorkOSUserId } from "@hazel/schema" -import { Duration, Effect, Exit, Layer, Metric, Option } from "effect" +import { ServiceMap, Duration, Effect, Exit, Layer, Metric, Option } from "effect" import { UserLookupCacheError } from "../errors.ts" import { userLookupCacheHits, userLookupCacheMisses, userLookupCacheOperationLatency } from "../metrics.ts" import { UserLookupCacheRequest, type UserLookupResult } from "./user-lookup-request.ts" @@ -23,9 +23,8 @@ export const USER_LOOKUP_CACHE_TTL = Duration.minutes(5) * Uses ResultPersistence for schema-based serialization and Redis backing. * Requires: Persistence.ResultPersistence (provided by RedisResultPersistenceLive or MemoryResultPersistenceLive) */ -export class UserLookupCache extends Effect.Service()("@hazel/auth/UserLookupCache", { - accessors: true, - scoped: Effect.gen(function* () { +export class UserLookupCache extends ServiceMap.Service()("@hazel/auth/UserLookupCache", { + make: Effect.gen(function* () { const persistence = yield* Persistence.ResultPersistence const store = yield* persistence.make({ diff --git a/packages/auth/src/config.ts b/packages/auth/src/config.ts index b38a4af52..4e1b7b322 100644 --- a/packages/auth/src/config.ts +++ b/packages/auth/src/config.ts @@ -1,4 +1,4 @@ -import { Config, Effect, Layer } from "effect" +import { ServiceMap, Config, Effect, Layer } from "effect" /** * Configuration for auth services. @@ -15,9 +15,8 @@ export interface AuthConfigShape { * Auth configuration service. * Provides WorkOS credentials from environment variables. */ -export class AuthConfig extends Effect.Service()("@hazel/auth/AuthConfig", { - accessors: true, - effect: Effect.gen(function* () { +export class AuthConfig extends ServiceMap.Service()("@hazel/auth/AuthConfig", { + make: Effect.gen(function* () { const workosApiKey = yield* Config.string("WORKOS_API_KEY") const workosClientId = yield* Config.string("WORKOS_CLIENT_ID") diff --git a/packages/auth/src/consumers/backend-auth.ts b/packages/auth/src/consumers/backend-auth.ts index 8dd24d7f2..897f95453 100644 --- a/packages/auth/src/consumers/backend-auth.ts +++ b/packages/auth/src/consumers/backend-auth.ts @@ -7,7 +7,7 @@ import { type WorkOSOrganizationId, type WorkOSUserId, } from "@hazel/schema" -import { Config, Effect, Layer, Option, Schema } from "effect" +import { ServiceMap, Config, Effect, Layer, Option, Schema } from "effect" import { TreeFormatter } from "effect/ParseResult" import { createRemoteJWKSet, jwtVerify } from "jose" import { WorkOSClient } from "../session/workos-client.ts" @@ -89,10 +89,9 @@ export const decodeInternalOrganizationIdFromWorkOS = (externalId: string) => * * This is used by the backend HTTP API and WebSocket RPC handlers. */ -export class BackendAuth extends Effect.Service()("@hazel/auth/BackendAuth", { - accessors: true, +export class BackendAuth extends ServiceMap.Service()("@hazel/auth/BackendAuth", { dependencies: [WorkOSClient.Default], - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const workos = yield* WorkOSClient const clientId = yield* Config.string("WORKOS_CLIENT_ID").pipe(Effect.orDie) const decodeClaims = decodeWorkOSJwtClaims @@ -123,7 +122,7 @@ export class BackendAuth extends Effect.Service()("@hazel/auth/Back }).pipe(Effect.as(undefined)), onSome: (externalId) => decodeInternalOrganizationIdFromWorkOS(externalId).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.logWarning( "Failed to decode WorkOS external organization ID", { diff --git a/packages/auth/src/consumers/proxy-auth.ts b/packages/auth/src/consumers/proxy-auth.ts index 333582a8f..61248552b 100644 --- a/packages/auth/src/consumers/proxy-auth.ts +++ b/packages/auth/src/consumers/proxy-auth.ts @@ -6,7 +6,7 @@ import { type WorkOSOrganizationId, type WorkOSUserId, } from "@hazel/schema" -import { Effect, Option, Schema } from "effect" +import { ServiceMap, Effect, Option, Schema } from "effect" import { TreeFormatter } from "effect/ParseResult" import { createRemoteJWKSet, jwtVerify } from "jose" import { UserLookupCache } from "../cache/user-lookup-cache.ts" @@ -16,7 +16,7 @@ import type { AuthenticatedUserContext } from "../types.ts" /** * Authentication error for proxy auth. */ -export class ProxyAuthenticationError extends Schema.TaggedError()( +export class ProxyAuthenticationError extends Schema.TaggedErrorClass()( "ProxyAuthenticationError", { message: Schema.String, @@ -35,10 +35,9 @@ export class ProxyAuthenticationError extends Schema.TaggedError()("@hazel/auth/ProxyAuth", { - accessors: true, +export class ProxyAuth extends ServiceMap.Service()("@hazel/auth/ProxyAuth", { dependencies: [UserLookupCache.Default, WorkOSClient.Default], - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const userLookupCache = yield* UserLookupCache const workos = yield* WorkOSClient const db = yield* Database.Database @@ -57,7 +56,7 @@ export class ProxyAuth extends Effect.Service()("@hazel/auth/ProxyAut }).pipe(Effect.as(undefined)), onSome: (externalId) => Schema.decodeUnknown(OrganizationId)(externalId).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.logWarning( "Failed to decode WorkOS external organization ID", { @@ -86,7 +85,7 @@ export class ProxyAuth extends Effect.Service()("@hazel/auth/ProxyAut const lookupUser = Effect.fn("ProxyAuth.lookupUser")(function* (workosUserId: WorkOSUserId) { // Check cache first const cached = yield* userLookupCache.get(workosUserId).pipe( - Effect.catchAll((error) => { + Effect.catch((error) => { // Log cache error but continue with database lookup return Effect.logWarning("User lookup cache error", error).pipe( Effect.map(() => Option.none<{ internalUserId: UserId }>()), @@ -123,7 +122,7 @@ export class ProxyAuth extends Effect.Service()("@hazel/auth/ProxyAut // Cache successful lookup if (Option.isSome(userOption)) { yield* userLookupCache.set(workosUserId, userOption.value.id).pipe( - Effect.catchAll((error) => + Effect.catch((error) => // Log cache error but don't fail the request Effect.logWarning("Failed to cache user lookup", error), ), diff --git a/packages/auth/src/errors.ts b/packages/auth/src/errors.ts index 7f726fea3..b564d0622 100644 --- a/packages/auth/src/errors.ts +++ b/packages/auth/src/errors.ts @@ -3,7 +3,7 @@ import { Schema } from "effect" /** * Error thrown when session cache operations fail */ -export class SessionCacheError extends Schema.TaggedError()("SessionCacheError", { +export class SessionCacheError extends Schema.TaggedErrorClass()("SessionCacheError", { message: Schema.String, cause: Schema.optional(Schema.Unknown), }) {} @@ -11,7 +11,7 @@ export class SessionCacheError extends Schema.TaggedError()(" /** * Error thrown when user lookup cache operations fail */ -export class UserLookupCacheError extends Schema.TaggedError()("UserLookupCacheError", { +export class UserLookupCacheError extends Schema.TaggedErrorClass()("UserLookupCacheError", { message: Schema.String, cause: Schema.optional(Schema.Unknown), }) {} @@ -19,7 +19,7 @@ export class UserLookupCacheError extends Schema.TaggedError()( +export class OrganizationFetchError extends Schema.TaggedErrorClass()( "OrganizationFetchError", { message: Schema.String, diff --git a/packages/auth/src/session/workos-client.ts b/packages/auth/src/session/workos-client.ts index d2d0f896d..eefc22008 100644 --- a/packages/auth/src/session/workos-client.ts +++ b/packages/auth/src/session/workos-client.ts @@ -3,17 +3,16 @@ import { WorkOSClientId, WorkOSOrganizationId, WorkOSUserId } from "@hazel/schem import type { Organization, User as WorkOSUser } from "@workos-inc/node" import { OrganizationFetchError } from "../errors.ts" import { WorkOS as WorkOSNodeAPI } from "@workos-inc/node" -import { Effect, Layer, Schema } from "effect" +import { ServiceMap, Effect, Layer, Schema } from "effect" import { AuthConfig } from "../config.ts" /** * WorkOS client wrapper with Effect integration. * Provides type-safe access to WorkOS SDK operations. */ -export class WorkOSClient extends Effect.Service()("@hazel/auth/WorkOSClient", { - accessors: true, +export class WorkOSClient extends ServiceMap.Service()("@hazel/auth/WorkOSClient", { dependencies: [AuthConfig.Default], - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const config = yield* AuthConfig const client = new WorkOSNodeAPI(config.workosApiKey, { clientId: config.workosClientId, diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index b23122550..0b85aedd5 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -13,7 +13,6 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@effect/platform": "catalog:effect", "@hazel/db": "workspace:*", "@hazel/domain": "workspace:*", "@hazel/schema": "workspace:*", @@ -22,7 +21,6 @@ "effect": "catalog:effect" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3" } diff --git a/packages/backend-core/src/repositories/attachment-repo.ts b/packages/backend-core/src/repositories/attachment-repo.ts index 09b78e0fd..3d93a2cf2 100644 --- a/packages/backend-core/src/repositories/attachment-repo.ts +++ b/packages/backend-core/src/repositories/attachment-repo.ts @@ -1,10 +1,9 @@ import { ModelRepository, schema } from "@hazel/db" import { Attachment } from "@hazel/domain/models" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" -export class AttachmentRepo extends Effect.Service()("AttachmentRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class AttachmentRepo extends ServiceMap.Service()("AttachmentRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository(schema.attachmentsTable, Attachment.Model, { idColumn: "id", name: "Attachment", diff --git a/packages/backend-core/src/repositories/bot-command-repo.ts b/packages/backend-core/src/repositories/bot-command-repo.ts index e2ece259a..e50eb9381 100644 --- a/packages/backend-core/src/repositories/bot-command-repo.ts +++ b/packages/backend-core/src/repositories/bot-command-repo.ts @@ -2,11 +2,10 @@ import { and, Database, eq, inArray, lt, ModelRepository, schema, type TxFn } fr import type { BotCommandId, BotId } from "@hazel/schema" import { BotCommand } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class BotCommandRepo extends Effect.Service()("BotCommandRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class BotCommandRepo extends ServiceMap.Service()("BotCommandRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository(schema.botCommandsTable, BotCommand.Model, { idColumn: "id", name: "BotCommand", diff --git a/packages/backend-core/src/repositories/bot-installation-repo.ts b/packages/backend-core/src/repositories/bot-installation-repo.ts index 82267a240..831da6d53 100644 --- a/packages/backend-core/src/repositories/bot-installation-repo.ts +++ b/packages/backend-core/src/repositories/bot-installation-repo.ts @@ -2,11 +2,10 @@ import { and, Database, eq, inArray, ModelRepository, schema, type TxFn } from " import type { BotId, BotInstallationId, OrganizationId } from "@hazel/schema" import { BotInstallation } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class BotInstallationRepo extends Effect.Service()("BotInstallationRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class BotInstallationRepo extends ServiceMap.Service()("BotInstallationRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.botInstallationsTable, BotInstallation.Model, diff --git a/packages/backend-core/src/repositories/bot-repo.ts b/packages/backend-core/src/repositories/bot-repo.ts index 96c7d4ea3..7e70e4d85 100644 --- a/packages/backend-core/src/repositories/bot-repo.ts +++ b/packages/backend-core/src/repositories/bot-repo.ts @@ -14,11 +14,10 @@ import { import type { BotId, UserId } from "@hazel/schema" import { Bot } from "@hazel/domain/models" -import { Effect, Option, type Schema } from "effect" +import { ServiceMap, Effect, Option, type Schema } from "effect" -export class BotRepo extends Effect.Service()("BotRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class BotRepo extends ServiceMap.Service()("BotRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository(schema.botsTable, Bot.Model, { idColumn: "id", name: "Bot", diff --git a/packages/backend-core/src/repositories/channel-member-repo.ts b/packages/backend-core/src/repositories/channel-member-repo.ts index c798db544..d8e6130ea 100644 --- a/packages/backend-core/src/repositories/channel-member-repo.ts +++ b/packages/backend-core/src/repositories/channel-member-repo.ts @@ -2,11 +2,10 @@ import { and, Database, eq, inArray, isNull, ModelRepository, schema, sql, type import type { ChannelId, OrganizationId, UserId } from "@hazel/schema" import { ChannelMember } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class ChannelMemberRepo extends Effect.Service()("ChannelMemberRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class ChannelMemberRepo extends ServiceMap.Service()("ChannelMemberRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.channelMembersTable, ChannelMember.Model, diff --git a/packages/backend-core/src/repositories/channel-repo.ts b/packages/backend-core/src/repositories/channel-repo.ts index 0a0b22b41..962d5ae27 100644 --- a/packages/backend-core/src/repositories/channel-repo.ts +++ b/packages/backend-core/src/repositories/channel-repo.ts @@ -2,11 +2,10 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import type { OrganizationId } from "@hazel/schema" import { Channel } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class ChannelRepo extends Effect.Service()("ChannelRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class ChannelRepo extends ServiceMap.Service()("ChannelRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository(schema.channelsTable, Channel.Model, { idColumn: "id", name: "Channel", diff --git a/packages/backend-core/src/repositories/channel-section-repo.ts b/packages/backend-core/src/repositories/channel-section-repo.ts index a73fcf5d1..271ce25d1 100644 --- a/packages/backend-core/src/repositories/channel-section-repo.ts +++ b/packages/backend-core/src/repositories/channel-section-repo.ts @@ -1,10 +1,9 @@ import { ModelRepository, schema } from "@hazel/db" import { ChannelSection } from "@hazel/domain/models" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" -export class ChannelSectionRepo extends Effect.Service()("ChannelSectionRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class ChannelSectionRepo extends ServiceMap.Service()("ChannelSectionRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.channelSectionsTable, ChannelSection.Model, diff --git a/packages/backend-core/src/repositories/channel-webhook-repo.ts b/packages/backend-core/src/repositories/channel-webhook-repo.ts index 4864ef7fd..654539153 100644 --- a/packages/backend-core/src/repositories/channel-webhook-repo.ts +++ b/packages/backend-core/src/repositories/channel-webhook-repo.ts @@ -2,11 +2,10 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import type { ChannelId, ChannelWebhookId, OrganizationId } from "@hazel/schema" import { ChannelWebhook } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class ChannelWebhookRepo extends Effect.Service()("ChannelWebhookRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class ChannelWebhookRepo extends ServiceMap.Service()("ChannelWebhookRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.channelWebhooksTable, ChannelWebhook.Model, diff --git a/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts b/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts index 1c425e47d..e5d7a7795 100644 --- a/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts @@ -2,12 +2,11 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import { ChatSyncChannelLink } from "@hazel/domain/models" import type { ChannelId, ExternalChannelId, SyncChannelLinkId, SyncConnectionId } from "@hazel/schema" -import { Effect, Option, Schema } from "effect" +import { ServiceMap, Effect, Option, Schema } from "effect" -export class ChatSyncChannelLinkRepo extends Effect.Service()( +export class ChatSyncChannelLinkRepo extends ServiceMap.Service()( "ChatSyncChannelLinkRepo", { - accessors: true, effect: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.chatSyncChannelLinksTable, diff --git a/packages/backend-core/src/repositories/chat-sync-connection-repo.ts b/packages/backend-core/src/repositories/chat-sync-connection-repo.ts index ed38b1be9..347c2ebea 100644 --- a/packages/backend-core/src/repositories/chat-sync-connection-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-connection-repo.ts @@ -2,12 +2,11 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import { ChatSyncConnection } from "@hazel/domain/models" import type { IntegrationConnectionId, OrganizationId, SyncConnectionId } from "@hazel/schema" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class ChatSyncConnectionRepo extends Effect.Service()( +export class ChatSyncConnectionRepo extends ServiceMap.Service()( "ChatSyncConnectionRepo", { - accessors: true, effect: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.chatSyncConnectionsTable, diff --git a/packages/backend-core/src/repositories/chat-sync-event-receipt-repo.ts b/packages/backend-core/src/repositories/chat-sync-event-receipt-repo.ts index 71b82b14d..064eb0782 100644 --- a/packages/backend-core/src/repositories/chat-sync-event-receipt-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-event-receipt-repo.ts @@ -2,12 +2,11 @@ import { and, Database, eq, gte, ModelRepository, schema, type TxFn } from "@haz import { ChatSyncEventReceipt } from "@hazel/domain/models" import type { SyncChannelLinkId, SyncConnectionId, SyncEventReceiptId } from "@hazel/schema" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class ChatSyncEventReceiptRepo extends Effect.Service()( +export class ChatSyncEventReceiptRepo extends ServiceMap.Service()( "ChatSyncEventReceiptRepo", { - accessors: true, effect: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.chatSyncEventReceiptsTable, diff --git a/packages/backend-core/src/repositories/chat-sync-message-link-repo.ts b/packages/backend-core/src/repositories/chat-sync-message-link-repo.ts index 89dfeb835..8a9a8d92f 100644 --- a/packages/backend-core/src/repositories/chat-sync-message-link-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-message-link-repo.ts @@ -8,12 +8,11 @@ import type { SyncChannelLinkId, SyncMessageLinkId, } from "@hazel/schema" -import { Effect, Option, Schema } from "effect" +import { ServiceMap, Effect, Option, Schema } from "effect" -export class ChatSyncMessageLinkRepo extends Effect.Service()( +export class ChatSyncMessageLinkRepo extends ServiceMap.Service()( "ChatSyncMessageLinkRepo", { - accessors: true, effect: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.chatSyncMessageLinksTable, diff --git a/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts b/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts index bf70adbaf..a2721a404 100644 --- a/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts +++ b/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts @@ -1,12 +1,11 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" import type { ChannelId, ConnectConversationId, OrganizationId } from "@hazel/schema" import { ConnectConversationChannel } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class ConnectConversationChannelRepo extends Effect.Service()( +export class ConnectConversationChannelRepo extends ServiceMap.Service()( "ConnectConversationChannelRepo", { - accessors: true, effect: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.connectConversationChannelsTable, diff --git a/packages/backend-core/src/repositories/connect-conversation-repo.ts b/packages/backend-core/src/repositories/connect-conversation-repo.ts index 06cb01f41..a6ede96e9 100644 --- a/packages/backend-core/src/repositories/connect-conversation-repo.ts +++ b/packages/backend-core/src/repositories/connect-conversation-repo.ts @@ -1,12 +1,11 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" import type { ChannelId } from "@hazel/schema" import { ConnectConversation } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class ConnectConversationRepo extends Effect.Service()( +export class ConnectConversationRepo extends ServiceMap.Service()( "ConnectConversationRepo", { - accessors: true, effect: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.connectConversationsTable, diff --git a/packages/backend-core/src/repositories/connect-invite-repo.ts b/packages/backend-core/src/repositories/connect-invite-repo.ts index 8c37250b3..a55d92912 100644 --- a/packages/backend-core/src/repositories/connect-invite-repo.ts +++ b/packages/backend-core/src/repositories/connect-invite-repo.ts @@ -1,11 +1,10 @@ import { and, Database, eq, isNull, ModelRepository, or, schema, type TxFn } from "@hazel/db" import type { ConnectInviteId, OrganizationId } from "@hazel/schema" import { ConnectInvite } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class ConnectInviteRepo extends Effect.Service()("ConnectInviteRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class ConnectInviteRepo extends ServiceMap.Service()("ConnectInviteRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.connectInvitesTable, ConnectInvite.Model, diff --git a/packages/backend-core/src/repositories/connect-participant-repo.ts b/packages/backend-core/src/repositories/connect-participant-repo.ts index c19a32ec3..c3777c8b5 100644 --- a/packages/backend-core/src/repositories/connect-participant-repo.ts +++ b/packages/backend-core/src/repositories/connect-participant-repo.ts @@ -1,12 +1,11 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" import type { ChannelId, ConnectConversationId, UserId } from "@hazel/schema" import { ConnectParticipant } from "@hazel/domain/models" -import { Effect, Option, type Schema as EffectSchema } from "effect" +import { ServiceMap, Effect, Option, type Schema as EffectSchema } from "effect" -export class ConnectParticipantRepo extends Effect.Service()( +export class ConnectParticipantRepo extends ServiceMap.Service()( "ConnectParticipantRepo", { - accessors: true, effect: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.connectParticipantsTable, diff --git a/packages/backend-core/src/repositories/custom-emoji-repo.ts b/packages/backend-core/src/repositories/custom-emoji-repo.ts index 3c9a91b35..4c334dd83 100644 --- a/packages/backend-core/src/repositories/custom-emoji-repo.ts +++ b/packages/backend-core/src/repositories/custom-emoji-repo.ts @@ -2,11 +2,10 @@ import { and, Database, eq, isNotNull, isNull, ModelRepository, schema } from "@ import type { CustomEmojiId, OrganizationId } from "@hazel/schema" import { CustomEmoji } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class CustomEmojiRepo extends Effect.Service()("CustomEmojiRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class CustomEmojiRepo extends ServiceMap.Service()("CustomEmojiRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository(schema.customEmojisTable, CustomEmoji.Model, { idColumn: "id", name: "CustomEmoji", diff --git a/packages/backend-core/src/repositories/github-subscription-repo.ts b/packages/backend-core/src/repositories/github-subscription-repo.ts index 3bd9529d7..065fabeee 100644 --- a/packages/backend-core/src/repositories/github-subscription-repo.ts +++ b/packages/backend-core/src/repositories/github-subscription-repo.ts @@ -2,12 +2,11 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import type { ChannelId, GitHubSubscriptionId, OrganizationId } from "@hazel/schema" import { GitHubSubscription } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class GitHubSubscriptionRepo extends Effect.Service()( +export class GitHubSubscriptionRepo extends ServiceMap.Service()( "GitHubSubscriptionRepo", { - accessors: true, effect: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.githubSubscriptionsTable, diff --git a/packages/backend-core/src/repositories/integration-connection-repo.ts b/packages/backend-core/src/repositories/integration-connection-repo.ts index 44068e3d5..05b132e1d 100644 --- a/packages/backend-core/src/repositories/integration-connection-repo.ts +++ b/packages/backend-core/src/repositories/integration-connection-repo.ts @@ -2,12 +2,11 @@ import { and, Database, eq, isNull, ModelRepository, schema, sql, type TxFn } fr import type { IntegrationConnectionId, OrganizationId, UserId } from "@hazel/schema" import { IntegrationConnection } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class IntegrationConnectionRepo extends Effect.Service()( +export class IntegrationConnectionRepo extends ServiceMap.Service()( "IntegrationConnectionRepo", { - accessors: true, effect: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.integrationConnectionsTable, diff --git a/packages/backend-core/src/repositories/integration-token-repo.ts b/packages/backend-core/src/repositories/integration-token-repo.ts index 319202e78..0f34d9b8e 100644 --- a/packages/backend-core/src/repositories/integration-token-repo.ts +++ b/packages/backend-core/src/repositories/integration-token-repo.ts @@ -2,11 +2,10 @@ import { Database, eq, ModelRepository, schema, type TxFn } from "@hazel/db" import type { IntegrationConnectionId, IntegrationTokenId } from "@hazel/schema" import { IntegrationToken } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class IntegrationTokenRepo extends Effect.Service()("IntegrationTokenRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class IntegrationTokenRepo extends ServiceMap.Service()("IntegrationTokenRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.integrationTokensTable, IntegrationToken.Model, diff --git a/packages/backend-core/src/repositories/invitation-repo.ts b/packages/backend-core/src/repositories/invitation-repo.ts index 37f005d45..294acc65e 100644 --- a/packages/backend-core/src/repositories/invitation-repo.ts +++ b/packages/backend-core/src/repositories/invitation-repo.ts @@ -2,11 +2,10 @@ import { and, Database, eq, lte, ModelRepository, schema, type TxFn } from "@haz import type { InvitationId, OrganizationId, WorkOSInvitationId } from "@hazel/schema" import { Invitation } from "@hazel/domain/models" -import { Effect, Option, type Schema } from "effect" +import { ServiceMap, Effect, Option, type Schema } from "effect" -export class InvitationRepo extends Effect.Service()("InvitationRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class InvitationRepo extends ServiceMap.Service()("InvitationRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository(schema.invitationsTable, Invitation.Model, { idColumn: "id", name: "Invitation", diff --git a/packages/backend-core/src/repositories/message-outbox-repo.ts b/packages/backend-core/src/repositories/message-outbox-repo.ts index 2f3b0e713..37a23276d 100644 --- a/packages/backend-core/src/repositories/message-outbox-repo.ts +++ b/packages/backend-core/src/repositories/message-outbox-repo.ts @@ -1,7 +1,7 @@ import { Database, and, asc, eq, inArray, or, schema, sql } from "@hazel/db" import type { DatabaseError, TxFn } from "@hazel/db" import { ChannelId, MessageId, MessageOutboxEventId, MessageReactionId, UserId } from "@hazel/schema" -import { Effect, Option, Schema } from "effect" +import { ServiceMap, Effect, Option, Schema } from "effect" export const MessageCreatedPayloadSchema = Schema.Struct({ messageId: MessageId, @@ -91,9 +91,8 @@ const InsertMessageOutboxEventSchema = Schema.Struct({ const InsertMessageOutboxEventArraySchema = Schema.Array(InsertMessageOutboxEventSchema) -export class MessageOutboxRepo extends Effect.Service()("MessageOutboxRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class MessageOutboxRepo extends ServiceMap.Service()("MessageOutboxRepo", { + make: Effect.gen(function* () { const db = yield* Database.Database const insert = (data: InsertMessageOutboxEvent, tx?: TxFn) => diff --git a/packages/backend-core/src/repositories/message-reaction-repo.ts b/packages/backend-core/src/repositories/message-reaction-repo.ts index 40624802d..aeb1a1d99 100644 --- a/packages/backend-core/src/repositories/message-reaction-repo.ts +++ b/packages/backend-core/src/repositories/message-reaction-repo.ts @@ -2,11 +2,10 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import type { ChannelId, ConnectConversationId, MessageId, UserId } from "@hazel/schema" import { MessageReaction } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class MessageReactionRepo extends Effect.Service()("MessageReactionRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class MessageReactionRepo extends ServiceMap.Service()("MessageReactionRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.messageReactionsTable, MessageReaction.Model, diff --git a/packages/backend-core/src/repositories/message-repo.ts b/packages/backend-core/src/repositories/message-repo.ts index 0551478ec..18f7fca3b 100644 --- a/packages/backend-core/src/repositories/message-repo.ts +++ b/packages/backend-core/src/repositories/message-repo.ts @@ -16,7 +16,7 @@ import { import type { ChannelId, ConnectConversationId, MessageId, OrganizationId, UserId } from "@hazel/schema" import { Message } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" export interface ListByChannelParams { channelId: ChannelId @@ -35,9 +35,8 @@ export interface ListByChannelParams { limit: number } -export class MessageRepo extends Effect.Service()("MessageRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class MessageRepo extends ServiceMap.Service()("MessageRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository(schema.messagesTable, Message.Model, { idColumn: "id", name: "Message", diff --git a/packages/backend-core/src/repositories/notification-repo.ts b/packages/backend-core/src/repositories/notification-repo.ts index 26b4d853d..598b6d343 100644 --- a/packages/backend-core/src/repositories/notification-repo.ts +++ b/packages/backend-core/src/repositories/notification-repo.ts @@ -2,11 +2,10 @@ import { and, Database, eq, inArray, ModelRepository, schema, type TxFn } from " import type { ChannelId, MessageId, OrganizationMemberId } from "@hazel/schema" import { Notification } from "@hazel/domain/models" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" -export class NotificationRepo extends Effect.Service()("NotificationRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class NotificationRepo extends ServiceMap.Service()("NotificationRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.notificationsTable, Notification.Model, diff --git a/packages/backend-core/src/repositories/organization-member-repo.ts b/packages/backend-core/src/repositories/organization-member-repo.ts index 77b98923b..6c92ed3b2 100644 --- a/packages/backend-core/src/repositories/organization-member-repo.ts +++ b/packages/backend-core/src/repositories/organization-member-repo.ts @@ -2,12 +2,11 @@ import { and, count, Database, eq, isNull, ModelRepository, schema, type TxFn } import type { OrganizationId, OrganizationMemberId, UserId } from "@hazel/schema" import { OrganizationMember } from "@hazel/domain/models" -import { Effect, Option, type Schema } from "effect" +import { ServiceMap, Effect, Option, type Schema } from "effect" -export class OrganizationMemberRepo extends Effect.Service()( +export class OrganizationMemberRepo extends ServiceMap.Service()( "OrganizationMemberRepo", { - accessors: true, effect: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.organizationMembersTable, diff --git a/packages/backend-core/src/repositories/organization-repo.ts b/packages/backend-core/src/repositories/organization-repo.ts index 09a5eb43f..62f3bf8ec 100644 --- a/packages/backend-core/src/repositories/organization-repo.ts +++ b/packages/backend-core/src/repositories/organization-repo.ts @@ -1,13 +1,12 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" import type { OrganizationId, UserId } from "@hazel/schema" import { Organization } from "@hazel/domain/models" -import { Effect, Option, type Schema } from "effect" +import { ServiceMap, Effect, Option, type Schema } from "effect" import { ChannelMemberRepo } from "./channel-member-repo" import { ChannelRepo } from "./channel-repo" -export class OrganizationRepo extends Effect.Service()("OrganizationRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class OrganizationRepo extends ServiceMap.Service()("OrganizationRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.organizationsTable, Organization.Model, diff --git a/packages/backend-core/src/repositories/pinned-message-repo.ts b/packages/backend-core/src/repositories/pinned-message-repo.ts index 4fcfc1bb9..c2bad2fae 100644 --- a/packages/backend-core/src/repositories/pinned-message-repo.ts +++ b/packages/backend-core/src/repositories/pinned-message-repo.ts @@ -1,10 +1,9 @@ import { ModelRepository, schema } from "@hazel/db" import { PinnedMessage } from "@hazel/domain/models" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" -export class PinnedMessageRepo extends Effect.Service()("PinnedMessageRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class PinnedMessageRepo extends ServiceMap.Service()("PinnedMessageRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.pinnedMessagesTable, PinnedMessage.Model, diff --git a/packages/backend-core/src/repositories/rss-subscription-repo.ts b/packages/backend-core/src/repositories/rss-subscription-repo.ts index 416539651..3ba8d3e51 100644 --- a/packages/backend-core/src/repositories/rss-subscription-repo.ts +++ b/packages/backend-core/src/repositories/rss-subscription-repo.ts @@ -2,11 +2,10 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import type { ChannelId, OrganizationId, RssSubscriptionId } from "@hazel/schema" import { RssSubscription } from "@hazel/domain/models" -import { Effect, Option } from "effect" +import { ServiceMap, Effect, Option } from "effect" -export class RssSubscriptionRepo extends Effect.Service()("RssSubscriptionRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class RssSubscriptionRepo extends ServiceMap.Service()("RssSubscriptionRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.rssSubscriptionsTable, RssSubscription.Model, diff --git a/packages/backend-core/src/repositories/typing-indicator-repo.ts b/packages/backend-core/src/repositories/typing-indicator-repo.ts index b1049f304..df1df19ff 100644 --- a/packages/backend-core/src/repositories/typing-indicator-repo.ts +++ b/packages/backend-core/src/repositories/typing-indicator-repo.ts @@ -2,11 +2,10 @@ import { and, Database, eq, lt, ModelRepository, schema, type TxFn } from "@haze import { ChannelId, ChannelMemberId, TypingIndicatorId } from "@hazel/schema" import { TypingIndicator } from "@hazel/domain/models" -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" -export class TypingIndicatorRepo extends Effect.Service()("TypingIndicatorRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class TypingIndicatorRepo extends ServiceMap.Service()("TypingIndicatorRepo", { + make: Effect.gen(function* () { const db = yield* Database.Database const baseRepo = yield* ModelRepository.makeRepository( schema.typingIndicatorsTable, diff --git a/packages/backend-core/src/repositories/user-presence-status-repo.ts b/packages/backend-core/src/repositories/user-presence-status-repo.ts index d2dd761da..4dcacb866 100644 --- a/packages/backend-core/src/repositories/user-presence-status-repo.ts +++ b/packages/backend-core/src/repositories/user-presence-status-repo.ts @@ -2,12 +2,11 @@ import { and, Database, eq, inArray, lt, ModelRepository, ne, schema, type TxFn import type { ChannelId, UserId } from "@hazel/schema" import { UserPresenceStatus } from "@hazel/domain/models" -import { Effect, Option, type Schema } from "effect" +import { ServiceMap, Effect, Option, type Schema } from "effect" -export class UserPresenceStatusRepo extends Effect.Service()( +export class UserPresenceStatusRepo extends ServiceMap.Service()( "UserPresenceStatusRepo", { - accessors: true, effect: Effect.gen(function* () { const db = yield* Database.Database const baseRepo = yield* ModelRepository.makeRepository( diff --git a/packages/backend-core/src/repositories/user-repo.ts b/packages/backend-core/src/repositories/user-repo.ts index 12c9c22dc..bf96ffae0 100644 --- a/packages/backend-core/src/repositories/user-repo.ts +++ b/packages/backend-core/src/repositories/user-repo.ts @@ -2,11 +2,10 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import type { UserId, WorkOSUserId } from "@hazel/schema" import { User } from "@hazel/domain/models" -import { Effect, Option, type Schema } from "effect" +import { ServiceMap, Effect, Option, type Schema } from "effect" -export class UserRepo extends Effect.Service()("UserRepo", { - accessors: true, - effect: Effect.gen(function* () { +export class UserRepo extends ServiceMap.Service()("UserRepo", { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository(schema.usersTable, User.Model, { idColumn: "id", name: "User", diff --git a/packages/backend-core/src/services/workos-sync.ts b/packages/backend-core/src/services/workos-sync.ts index 1ba66f93a..e461bcf12 100644 --- a/packages/backend-core/src/services/workos-sync.ts +++ b/packages/backend-core/src/services/workos-sync.ts @@ -8,7 +8,7 @@ import { type UserId, } from "@hazel/schema" import type { Event } from "@workos-inc/node" -import { Effect, Match, Option, pipe, Schema, Stream } from "effect" +import { ServiceMap, Effect, Match, Option, pipe, Schema, Stream } from "effect" import { TreeFormatter } from "effect/ParseResult" import { InvitationRepo } from "../repositories/invitation-repo" import { OrganizationMemberRepo } from "../repositories/organization-member-repo" @@ -17,7 +17,7 @@ import { UserRepo } from "../repositories/user-repo" import { WorkOSClient } from "./workos" // Error types -export class WorkOSSyncError extends Schema.TaggedError("WorkOSSyncError")( +export class WorkOSSyncError extends Schema.TaggedErrorClass("WorkOSSyncError")( "WorkOSSyncError", { message: Schema.String, @@ -86,9 +86,8 @@ export const normalizeWorkOSRole = ( Effect.orElseSucceed((): Schema.Schema.Type => "member"), ) -export class WorkOSSync extends Effect.Service()("WorkOSSync", { - accessors: true, - effect: Effect.gen(function* () { +export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { + make: Effect.gen(function* () { const workos = yield* WorkOSClient const db = yield* Database.Database const userRepo = yield* UserRepo @@ -976,7 +975,7 @@ export class WorkOSSync extends Effect.Service()("WorkOSSync", { ), Effect.tap(() => Effect.logDebug(`Successfully processed: ${event.event}`)), Effect.as({ success: true }), - Effect.catchAll((error) => + Effect.catch((error) => Effect.logError(`Failed to process ${event.event}`, { error }).pipe( Effect.as({ success: false, error: String(error) }), ), diff --git a/packages/backend-core/src/services/workos.ts b/packages/backend-core/src/services/workos.ts index 86d896116..bb932ff88 100644 --- a/packages/backend-core/src/services/workos.ts +++ b/packages/backend-core/src/services/workos.ts @@ -1,13 +1,12 @@ import { WorkOS as WorkOSNodeAPI } from "@workos-inc/node" -import { Config, Effect, Redacted, Schema } from "effect" +import { ServiceMap, Config, Effect, Redacted, Schema } from "effect" -export class WorkOSApiError extends Schema.TaggedError()("WorkOSApiError", { +export class WorkOSApiError extends Schema.TaggedErrorClass()("WorkOSApiError", { cause: Schema.Unknown, }) {} -export class WorkOSClient extends Effect.Service()("WorkOSClient", { - accessors: true, - effect: Effect.gen(function* () { +export class WorkOSClient extends ServiceMap.Service()("WorkOSClient", { + make: Effect.gen(function* () { const apiKey = yield* Config.redacted("WORKOS_API_KEY") const clientId = yield* Config.string("WORKOS_CLIENT_ID") diff --git a/packages/db/package.json b/packages/db/package.json index b8af20dd5..bb190b750 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -17,7 +17,6 @@ "db:studio": "drizzle-kit studio" }, "dependencies": { - "@effect/experimental": "catalog:effect", "@hazel/domain": "workspace:*", "@hazel/schema": "workspace:*", "drizzle-orm": "^0.45.1", @@ -25,7 +24,6 @@ "postgres": "^3.4.7" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@testcontainers/postgresql": "^10.18.0", "@types/bun": "1.3.9", "drizzle-kit": "^0.31.8", diff --git a/packages/db/src/services/database.ts b/packages/db/src/services/database.ts index 8c94b0eb3..dd8d875be 100644 --- a/packages/db/src/services/database.ts +++ b/packages/db/src/services/database.ts @@ -43,7 +43,7 @@ const DatabaseErrorType = Schema.Literal( "query_error", ) -export class DatabaseError extends Schema.TaggedError()("DatabaseError", { +export class DatabaseError extends Schema.TaggedErrorClass()("DatabaseError", { type: DatabaseErrorType, cause: Schema.Unknown, }) { @@ -72,7 +72,7 @@ const matchPgError = (error: unknown) => { return null } -export class DatabaseConnectionLostError extends Schema.TaggedError()( +export class DatabaseConnectionLostError extends Schema.TaggedErrorClass()( "DatabaseConnectionLostError", { cause: Schema.Unknown, diff --git a/packages/db/src/services/model.ts b/packages/db/src/services/model.ts index 543316459..26def92a1 100644 --- a/packages/db/src/services/model.ts +++ b/packages/db/src/services/model.ts @@ -19,7 +19,7 @@ export interface RepositoryOptions { export type PartialExcept = Partial> & Pick -export class EntityNotFound extends Schema.TaggedError()("EntityNotFound", { +export class EntityNotFound extends Schema.TaggedErrorClass()("EntityNotFound", { type: Schema.String, id: Schema.Any, }) {} diff --git a/packages/domain/package.json b/packages/domain/package.json index 0adccb463..aa7b17f1f 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -15,18 +15,12 @@ "./scopes": "./src/scopes/index.ts" }, "dependencies": { - "@effect/cluster": "catalog:effect", - "@effect/experimental": "catalog:effect", - "@effect/platform": "catalog:effect", - "@effect/rpc": "catalog:effect", - "@effect/workflow": "catalog:effect", "@hazel/integrations": "workspace:*", "@hazel/schema": "workspace:*", "drizzle-orm": "^0.45.1", "effect": "catalog:effect" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3" } diff --git a/packages/domain/src/cluster/activities/bot-activities.ts b/packages/domain/src/cluster/activities/bot-activities.ts index 9a52da747..562a675b9 100644 --- a/packages/domain/src/cluster/activities/bot-activities.ts +++ b/packages/domain/src/cluster/activities/bot-activities.ts @@ -1,7 +1,7 @@ import { Schema } from "effect" // Error types for bot user activities -export class BotUserQueryError extends Schema.TaggedError()("BotUserQueryError", { +export class BotUserQueryError extends Schema.TaggedErrorClass()("BotUserQueryError", { provider: Schema.String, message: Schema.String, cause: Schema.Unknown.pipe(Schema.optional), diff --git a/packages/domain/src/cluster/activities/cleanup-activities.ts b/packages/domain/src/cluster/activities/cleanup-activities.ts index 92fd881a1..a5717b9f5 100644 --- a/packages/domain/src/cluster/activities/cleanup-activities.ts +++ b/packages/domain/src/cluster/activities/cleanup-activities.ts @@ -28,7 +28,7 @@ export const MarkUploadsFailedResult = Schema.Struct({ export type MarkUploadsFailedResult = typeof MarkUploadsFailedResult.Type // Error types for cleanup activities -export class FindStaleUploadsError extends Schema.TaggedError()( +export class FindStaleUploadsError extends Schema.TaggedErrorClass()( "FindStaleUploadsError", { message: Schema.String, @@ -38,7 +38,7 @@ export class FindStaleUploadsError extends Schema.TaggedError()( +export class MarkUploadsFailedError extends Schema.TaggedErrorClass()( "MarkUploadsFailedError", { message: Schema.String, diff --git a/packages/domain/src/cluster/activities/github-activities.ts b/packages/domain/src/cluster/activities/github-activities.ts index 4120878d2..30ec6f768 100644 --- a/packages/domain/src/cluster/activities/github-activities.ts +++ b/packages/domain/src/cluster/activities/github-activities.ts @@ -33,7 +33,7 @@ export const CreateGitHubMessagesResult = Schema.Struct({ export type CreateGitHubMessagesResult = Schema.Schema.Type // Error types for GitHub activities -export class GetGitHubSubscriptionsError extends Schema.TaggedError()( +export class GetGitHubSubscriptionsError extends Schema.TaggedErrorClass()( "GetGitHubSubscriptionsError", { repositoryId: Schema.Number, @@ -44,7 +44,7 @@ export class GetGitHubSubscriptionsError extends Schema.TaggedError()( +export class CreateGitHubMessageError extends Schema.TaggedErrorClass()( "CreateGitHubMessageError", { channelId: ChannelId, diff --git a/packages/domain/src/cluster/activities/github-installation-activities.ts b/packages/domain/src/cluster/activities/github-installation-activities.ts index 6ad8ca2c0..c7f6efbe4 100644 --- a/packages/domain/src/cluster/activities/github-installation-activities.ts +++ b/packages/domain/src/cluster/activities/github-installation-activities.ts @@ -27,7 +27,7 @@ export const UpdateConnectionStatusResult = Schema.Struct({ export type UpdateConnectionStatusResult = Schema.Schema.Type // Error types for installation activities -export class FindConnectionByInstallationError extends Schema.TaggedError()( +export class FindConnectionByInstallationError extends Schema.TaggedErrorClass()( "FindConnectionByInstallationError", { installationId: Schema.Number, @@ -38,7 +38,7 @@ export class FindConnectionByInstallationError extends Schema.TaggedError()( +export class UpdateConnectionStatusError extends Schema.TaggedErrorClass()( "UpdateConnectionStatusError", { installationId: Schema.Number, diff --git a/packages/domain/src/cluster/activities/message-activities.ts b/packages/domain/src/cluster/activities/message-activities.ts index 05c6082c5..37dc64e89 100644 --- a/packages/domain/src/cluster/activities/message-activities.ts +++ b/packages/domain/src/cluster/activities/message-activities.ts @@ -45,7 +45,7 @@ export const CreateNotificationsResult = Schema.Struct({ export type CreateNotificationsResult = typeof CreateNotificationsResult.Type // Error types for message activities -export class GetChannelMembersError extends Schema.TaggedError()( +export class GetChannelMembersError extends Schema.TaggedErrorClass()( "GetChannelMembersError", { channelId: ChannelId, @@ -56,7 +56,7 @@ export class GetChannelMembersError extends Schema.TaggedError()( +export class CreateNotificationError extends Schema.TaggedErrorClass()( "CreateNotificationError", { messageId: MessageId, diff --git a/packages/domain/src/cluster/activities/rss-activities.ts b/packages/domain/src/cluster/activities/rss-activities.ts index 9b5eba879..52959c065 100644 --- a/packages/domain/src/cluster/activities/rss-activities.ts +++ b/packages/domain/src/cluster/activities/rss-activities.ts @@ -42,7 +42,7 @@ export const PostRssItemsResult = Schema.Struct({ export type PostRssItemsResult = Schema.Schema.Type // Error types for RSS activities -export class FetchRssFeedError extends Schema.TaggedError()("FetchRssFeedError", { +export class FetchRssFeedError extends Schema.TaggedErrorClass()("FetchRssFeedError", { subscriptionId: Schema.String, feedUrl: Schema.String, message: Schema.String, @@ -51,7 +51,7 @@ export class FetchRssFeedError extends Schema.TaggedError()(" readonly retryable = true } -export class PostRssItemsError extends Schema.TaggedError()("PostRssItemsError", { +export class PostRssItemsError extends Schema.TaggedErrorClass()("PostRssItemsError", { channelId: ChannelId, message: Schema.String, cause: Schema.Unknown.pipe(Schema.optional), @@ -59,7 +59,7 @@ export class PostRssItemsError extends Schema.TaggedError()(" readonly retryable = true } -export class UpdateSubscriptionStateError extends Schema.TaggedError()( +export class UpdateSubscriptionStateError extends Schema.TaggedErrorClass()( "UpdateSubscriptionStateError", { subscriptionId: Schema.String, diff --git a/packages/domain/src/cluster/activities/thread-naming-activities.ts b/packages/domain/src/cluster/activities/thread-naming-activities.ts index aab55cd87..c87faf769 100644 --- a/packages/domain/src/cluster/activities/thread-naming-activities.ts +++ b/packages/domain/src/cluster/activities/thread-naming-activities.ts @@ -43,7 +43,7 @@ export type UpdateThreadNameResult = typeof UpdateThreadNameResult.Type // ============================================================================ /** Thread channel does not exist */ -export class ThreadChannelNotFoundError extends Schema.TaggedError()( +export class ThreadChannelNotFoundError extends Schema.TaggedErrorClass()( "ThreadChannelNotFoundError", { threadChannelId: ChannelId }, ) { @@ -51,7 +51,7 @@ export class ThreadChannelNotFoundError extends Schema.TaggedError()( +export class OriginalMessageNotFoundError extends Schema.TaggedErrorClass()( "OriginalMessageNotFoundError", { threadChannelId: ChannelId, messageId: MessageId }, ) { @@ -59,7 +59,7 @@ export class OriginalMessageNotFoundError extends Schema.TaggedError()( +export class ThreadContextQueryError extends Schema.TaggedErrorClass()( "ThreadContextQueryError", { threadChannelId: ChannelId, @@ -75,7 +75,7 @@ export class ThreadContextQueryError extends Schema.TaggedError()( +export class AIProviderUnavailableError extends Schema.TaggedErrorClass()( "AIProviderUnavailableError", { provider: Schema.String, cause: Schema.Unknown.pipe(Schema.optional) }, ) { @@ -83,7 +83,7 @@ export class AIProviderUnavailableError extends Schema.TaggedError()("AIRateLimitError", { +export class AIRateLimitError extends Schema.TaggedErrorClass()("AIRateLimitError", { provider: Schema.String, retryAfter: Schema.Number.pipe(Schema.optional), }) { @@ -91,7 +91,7 @@ export class AIRateLimitError extends Schema.TaggedError()("AI } /** AI response could not be parsed or was empty */ -export class AIResponseParseError extends Schema.TaggedError()("AIResponseParseError", { +export class AIResponseParseError extends Schema.TaggedErrorClass()("AIResponseParseError", { threadChannelId: ChannelId, rawResponse: Schema.String.pipe(Schema.optional), }) { @@ -103,7 +103,7 @@ export class AIResponseParseError extends Schema.TaggedError()( +export class ThreadNameUpdateError extends Schema.TaggedErrorClass()( "ThreadNameUpdateError", { threadChannelId: ChannelId, newName: Schema.String, cause: Schema.Unknown.pipe(Schema.optional) }, ) { diff --git a/packages/domain/src/cluster/api.ts b/packages/domain/src/cluster/api.ts index cfa1203fb..87fe8d7b5 100644 --- a/packages/domain/src/cluster/api.ts +++ b/packages/domain/src/cluster/api.ts @@ -1,5 +1,5 @@ -import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform" -import { WorkflowProxy } from "@effect/workflow" +import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" +import { WorkflowProxy } from "effect/unstable/workflow" import { Schema } from "effect" import { CleanupUploadsWorkflow, diff --git a/packages/domain/src/cluster/workflows/cleanup-uploads-workflow.ts b/packages/domain/src/cluster/workflows/cleanup-uploads-workflow.ts index 565905be5..29442b8c5 100644 --- a/packages/domain/src/cluster/workflows/cleanup-uploads-workflow.ts +++ b/packages/domain/src/cluster/workflows/cleanup-uploads-workflow.ts @@ -1,4 +1,4 @@ -import { Workflow } from "@effect/workflow" +import { Workflow } from "effect/unstable/workflow" import { Schema } from "effect" import { CleanupUploadsWorkflowError } from "../activities/cleanup-activities.ts" diff --git a/packages/domain/src/cluster/workflows/github-installation-workflow.ts b/packages/domain/src/cluster/workflows/github-installation-workflow.ts index f9f994b1a..ad904611d 100644 --- a/packages/domain/src/cluster/workflows/github-installation-workflow.ts +++ b/packages/domain/src/cluster/workflows/github-installation-workflow.ts @@ -1,4 +1,4 @@ -import { Workflow } from "@effect/workflow" +import { Workflow } from "effect/unstable/workflow" import { Schema } from "effect" import { GitHubInstallationWorkflowError } from "../activities/github-installation-activities.ts" diff --git a/packages/domain/src/cluster/workflows/github-webhook-workflow.ts b/packages/domain/src/cluster/workflows/github-webhook-workflow.ts index b918b45d8..37205c98c 100644 --- a/packages/domain/src/cluster/workflows/github-webhook-workflow.ts +++ b/packages/domain/src/cluster/workflows/github-webhook-workflow.ts @@ -1,4 +1,4 @@ -import { Workflow } from "@effect/workflow" +import { Workflow } from "effect/unstable/workflow" import { Schema } from "effect" import { GitHubWebhookWorkflowError } from "../activities/github-activities.ts" diff --git a/packages/domain/src/cluster/workflows/message-notification-workflow.ts b/packages/domain/src/cluster/workflows/message-notification-workflow.ts index 34c5ec689..5f6492daa 100644 --- a/packages/domain/src/cluster/workflows/message-notification-workflow.ts +++ b/packages/domain/src/cluster/workflows/message-notification-workflow.ts @@ -1,4 +1,4 @@ -import { Workflow } from "@effect/workflow" +import { Workflow } from "effect/unstable/workflow" import { ChannelId, MessageId, UserId } from "@hazel/schema" import { Schema } from "effect" import { ChannelType } from "../../models/channel-model.ts" diff --git a/packages/domain/src/cluster/workflows/rss-feed-poll-workflow.ts b/packages/domain/src/cluster/workflows/rss-feed-poll-workflow.ts index 3e64e499d..aede33561 100644 --- a/packages/domain/src/cluster/workflows/rss-feed-poll-workflow.ts +++ b/packages/domain/src/cluster/workflows/rss-feed-poll-workflow.ts @@ -1,4 +1,4 @@ -import { Workflow } from "@effect/workflow" +import { Workflow } from "effect/unstable/workflow" import { ChannelId, OrganizationId, RssSubscriptionId } from "@hazel/schema" import { Schema } from "effect" import { RssFeedPollWorkflowError } from "../activities/rss-activities.ts" diff --git a/packages/domain/src/cluster/workflows/thread-naming-workflow.ts b/packages/domain/src/cluster/workflows/thread-naming-workflow.ts index 63bcc1ed3..bce90d162 100644 --- a/packages/domain/src/cluster/workflows/thread-naming-workflow.ts +++ b/packages/domain/src/cluster/workflows/thread-naming-workflow.ts @@ -1,4 +1,4 @@ -import { Workflow } from "@effect/workflow" +import { Workflow } from "effect/unstable/workflow" import { ChannelId, MessageId } from "@hazel/schema" import { ThreadNamingWorkflowError } from "../activities/thread-naming-activities" diff --git a/packages/domain/src/current-user.ts b/packages/domain/src/current-user.ts index 7c59ff409..5c5d8c0c4 100644 --- a/packages/domain/src/current-user.ts +++ b/packages/domain/src/current-user.ts @@ -1,4 +1,4 @@ -import { HttpApiMiddleware, HttpApiSecurity } from "@effect/platform" +import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" import { Context as C, Schema as S } from "effect" import { UnauthorizedError } from "./errors" import { OrganizationId, UserId } from "@hazel/schema" diff --git a/packages/domain/src/desktop-auth-errors.ts b/packages/domain/src/desktop-auth-errors.ts index f4c00e503..2d9a37a32 100644 --- a/packages/domain/src/desktop-auth-errors.ts +++ b/packages/domain/src/desktop-auth-errors.ts @@ -12,7 +12,7 @@ import { Schema } from "effect" /** * Tauri API not available in the current environment */ -export class TauriNotAvailableError extends Schema.TaggedError()( +export class TauriNotAvailableError extends Schema.TaggedErrorClass()( "TauriNotAvailableError", { message: Schema.String, @@ -23,7 +23,7 @@ export class TauriNotAvailableError extends Schema.TaggedError()("TauriCommandError", { +export class TauriCommandError extends Schema.TaggedErrorClass()("TauriCommandError", { message: Schema.String, command: Schema.String, detail: Schema.optional(Schema.String), @@ -36,14 +36,14 @@ export class TauriCommandError extends Schema.TaggedError()(" /** * OAuth callback timed out waiting for user to complete authentication */ -export class OAuthTimeoutError extends Schema.TaggedError()("OAuthTimeoutError", { +export class OAuthTimeoutError extends Schema.TaggedErrorClass()("OAuthTimeoutError", { message: Schema.String, }) {} /** * OAuth provider returned an error during authentication */ -export class OAuthCallbackError extends Schema.TaggedError()("OAuthCallbackError", { +export class OAuthCallbackError extends Schema.TaggedErrorClass()("OAuthCallbackError", { message: Schema.String, error: Schema.String, errorDescription: Schema.optional(Schema.String), @@ -52,7 +52,7 @@ export class OAuthCallbackError extends Schema.TaggedError() /** * No authorization code was received from OAuth callback */ -export class MissingAuthCodeError extends Schema.TaggedError()("MissingAuthCodeError", { +export class MissingAuthCodeError extends Schema.TaggedErrorClass()("MissingAuthCodeError", { message: Schema.String, }) {} @@ -63,7 +63,7 @@ export class MissingAuthCodeError extends Schema.TaggedError()("TokenStoreError", { +export class TokenStoreError extends Schema.TaggedErrorClass()("TokenStoreError", { message: Schema.String, operation: Schema.Literal("load", "get", "set", "delete"), detail: Schema.optional(Schema.String), @@ -72,7 +72,7 @@ export class TokenStoreError extends Schema.TaggedError()("Toke /** * A required token was not found in the store */ -export class TokenNotFoundError extends Schema.TaggedError()("TokenNotFoundError", { +export class TokenNotFoundError extends Schema.TaggedErrorClass()("TokenNotFoundError", { message: Schema.String, tokenType: Schema.Literal("access", "refresh", "expiresAt"), }) {} @@ -84,7 +84,7 @@ export class TokenNotFoundError extends Schema.TaggedError() /** * Failed to exchange authorization code for tokens */ -export class TokenExchangeError extends Schema.TaggedError()("TokenExchangeError", { +export class TokenExchangeError extends Schema.TaggedErrorClass()("TokenExchangeError", { message: Schema.String, detail: Schema.optional(Schema.String), }) {} @@ -92,7 +92,7 @@ export class TokenExchangeError extends Schema.TaggedError() /** * Failed to decode token response from server */ -export class TokenDecodeError extends Schema.TaggedError()("TokenDecodeError", { +export class TokenDecodeError extends Schema.TaggedErrorClass()("TokenDecodeError", { message: Schema.String, detail: Schema.optional(Schema.String), }) {} @@ -104,7 +104,7 @@ export class TokenDecodeError extends Schema.TaggedError()("To /** * Failed to connect to the desktop app's local OAuth server */ -export class DesktopConnectionError extends Schema.TaggedError()( +export class DesktopConnectionError extends Schema.TaggedErrorClass()( "DesktopConnectionError", { message: Schema.String, @@ -116,7 +116,7 @@ export class DesktopConnectionError extends Schema.TaggedError()( +export class InvalidDesktopStateError extends Schema.TaggedErrorClass()( "InvalidDesktopStateError", { message: Schema.String, diff --git a/packages/domain/src/errors.ts b/packages/domain/src/errors.ts index 88852eddb..fa4009655 100644 --- a/packages/domain/src/errors.ts +++ b/packages/domain/src/errors.ts @@ -1,16 +1,14 @@ -import { HttpApiSchema } from "@effect/platform" +import { HttpApiSchema } from "effect/unstable/httpapi" import { Effect, Predicate, Schema } from "effect" import { ChannelId, MessageId } from "@hazel/schema" -export class UnauthorizedError extends Schema.TaggedError("UnauthorizedError")( +export class UnauthorizedError extends Schema.TaggedErrorClass("UnauthorizedError")( "UnauthorizedError", { message: Schema.String, detail: Schema.String, }, - HttpApiSchema.annotations({ - status: 401, - }), + HttpApiSchema.status(401), ) { static is(u: unknown): u is UnauthorizedError { return Predicate.isTagged(u, "UnauthorizedError") @@ -21,33 +19,29 @@ export class UnauthorizedError extends Schema.TaggedError("Un * Error thrown when an OAuth authorization code has expired or has already been used. * This is a specific 401 error that indicates the user must restart the OAuth flow. */ -export class OAuthCodeExpiredError extends Schema.TaggedError("OAuthCodeExpiredError")( +export class OAuthCodeExpiredError extends Schema.TaggedErrorClass("OAuthCodeExpiredError")( "OAuthCodeExpiredError", { message: Schema.String, }, - HttpApiSchema.annotations({ - status: 401, - }), + HttpApiSchema.status(401), ) { static is(u: unknown): u is OAuthCodeExpiredError { return Predicate.isTagged(u, "OAuthCodeExpiredError") } } -export class InternalServerError extends Schema.TaggedError("InternalServerError")( +export class InternalServerError extends Schema.TaggedErrorClass("InternalServerError")( "InternalServerError", { message: Schema.String, detail: Schema.optional(Schema.String), cause: Schema.optional(Schema.Any), }, - HttpApiSchema.annotations({ - status: 500, - }), + HttpApiSchema.status(500), ) {} -export class WorkflowInitializationError extends Schema.TaggedError( +export class WorkflowInitializationError extends Schema.TaggedErrorClass( "WorkflowInitializationError", )( "WorkflowInitializationError", @@ -55,12 +49,10 @@ export class WorkflowInitializationError extends Schema.TaggedError( +export class DmChannelAlreadyExistsError extends Schema.TaggedErrorClass( "DmChannelAlreadyExistsError", )( "DmChannelAlreadyExistsError", @@ -68,44 +60,38 @@ export class DmChannelAlreadyExistsError extends Schema.TaggedError("MessageNotFoundError")( +export class MessageNotFoundError extends Schema.TaggedErrorClass("MessageNotFoundError")( "MessageNotFoundError", { messageId: MessageId, }, - HttpApiSchema.annotations({ - status: 404, - }), + HttpApiSchema.status(404), ) {} /** * Error thrown when attempting to create a thread within a thread. * Nested threads are not supported. */ -export class NestedThreadError extends Schema.TaggedError("NestedThreadError")( +export class NestedThreadError extends Schema.TaggedErrorClass("NestedThreadError")( "NestedThreadError", { channelId: ChannelId, }, - HttpApiSchema.annotations({ - status: 400, - }), + HttpApiSchema.status(400), ) {} /** * Error thrown when the workflow service is unreachable or unavailable. * Used when the cluster service cannot be contacted. */ -export class WorkflowServiceUnavailableError extends Schema.TaggedError( +export class WorkflowServiceUnavailableError extends Schema.TaggedErrorClass( "WorkflowServiceUnavailableError", )( "WorkflowServiceUnavailableError", @@ -113,9 +99,7 @@ export class WorkflowServiceUnavailableError extends Schema.TaggedError( diff --git a/packages/domain/src/http/api-v1/messages.ts b/packages/domain/src/http/api-v1/messages.ts index 058038e12..c1c12c8d6 100644 --- a/packages/domain/src/http/api-v1/messages.ts +++ b/packages/domain/src/http/api-v1/messages.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" import { InternalServerError, MessageNotFoundError, UnauthorizedError } from "../../errors" import { AttachmentId, ChannelId, MessageId } from "@hazel/schema" @@ -71,20 +71,20 @@ export class ToggleReactionResponse extends Schema.Class // ============ ERROR TYPES ============ -export class ChannelNotFoundError extends Schema.TaggedError()( +export class ChannelNotFoundError extends Schema.TaggedErrorClass()( "ChannelNotFoundError", { channelId: ChannelId, }, - HttpApiSchema.annotations({ status: 404 }), + HttpApiSchema.status(404), ) {} -export class InvalidPaginationError extends Schema.TaggedError()( +export class InvalidPaginationError extends Schema.TaggedErrorClass()( "InvalidPaginationError", { message: Schema.String, }, - HttpApiSchema.annotations({ status: 400 }), + HttpApiSchema.status(400), ) {} // ============ API GROUP ============ diff --git a/packages/domain/src/http/api.ts b/packages/domain/src/http/api.ts index 00980464a..85e2154c2 100644 --- a/packages/domain/src/http/api.ts +++ b/packages/domain/src/http/api.ts @@ -1,4 +1,4 @@ -import { HttpApi, OpenApi } from "@effect/platform" +import { HttpApi, OpenApi } from "effect/unstable/httpapi" import { ChatSyncGroup } from "./chat-sync" import { MessagesApiGroup } from "./api-v1/messages" import { AuthGroup } from "./auth" diff --git a/packages/domain/src/http/auth.ts b/packages/domain/src/http/auth.ts index 455d90db0..1b7d69a60 100644 --- a/packages/domain/src/http/auth.ts +++ b/packages/domain/src/http/auth.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" import { InternalServerError, OAuthCodeExpiredError, UnauthorizedError } from "../errors" import { OrganizationId } from "@hazel/schema" diff --git a/packages/domain/src/http/bot-commands.ts b/packages/domain/src/http/bot-commands.ts index 0805f4380..49c54d63f 100644 --- a/packages/domain/src/http/bot-commands.ts +++ b/packages/domain/src/http/bot-commands.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" import * as CurrentUser from "../current-user" import { InternalServerError, UnauthorizedError } from "../errors" @@ -65,16 +65,16 @@ export class BotMeResponse extends Schema.Class("BotMeResponse")( // ============ ERROR TYPES ============ -export class BotNotFoundError extends Schema.TaggedError()("BotNotFoundError", { +export class BotNotFoundError extends Schema.TaggedErrorClass()("BotNotFoundError", { botId: BotId, }) {} -export class BotNotInstalledError extends Schema.TaggedError()("BotNotInstalledError", { +export class BotNotInstalledError extends Schema.TaggedErrorClass()("BotNotInstalledError", { botId: BotId, orgId: OrganizationId, }) {} -export class BotCommandNotFoundError extends Schema.TaggedError()( +export class BotCommandNotFoundError extends Schema.TaggedErrorClass()( "BotCommandNotFoundError", { botId: BotId, @@ -82,7 +82,7 @@ export class BotCommandNotFoundError extends Schema.TaggedError()( +export class BotCommandExecutionError extends Schema.TaggedErrorClass()( "BotCommandExecutionError", { commandName: Schema.String, @@ -120,7 +120,7 @@ export class UpdateBotSettingsResponse extends Schema.Class()( +export class IntegrationNotAllowedError extends Schema.TaggedErrorClass()( "IntegrationNotAllowedError", { botId: BotId, diff --git a/packages/domain/src/http/chat-sync.ts b/packages/domain/src/http/chat-sync.ts index 0ca641406..5e44abcef 100644 --- a/packages/domain/src/http/chat-sync.ts +++ b/packages/domain/src/http/chat-sync.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { ChannelId, ExternalChannelId, @@ -44,21 +44,21 @@ export class ChatSyncDeleteResponse extends Schema.Class transactionId: TransactionId, }) {} -export class ChatSyncConnectionNotFoundError extends Schema.TaggedError()( +export class ChatSyncConnectionNotFoundError extends Schema.TaggedErrorClass()( "ChatSyncConnectionNotFoundError", { syncConnectionId: SyncConnectionId, }, ) {} -export class ChatSyncChannelLinkNotFoundError extends Schema.TaggedError()( +export class ChatSyncChannelLinkNotFoundError extends Schema.TaggedErrorClass()( "ChatSyncChannelLinkNotFoundError", { syncChannelLinkId: SyncChannelLinkId, }, ) {} -export class ChatSyncConnectionExistsError extends Schema.TaggedError()( +export class ChatSyncConnectionExistsError extends Schema.TaggedErrorClass()( "ChatSyncConnectionExistsError", { organizationId: OrganizationId, @@ -67,7 +67,7 @@ export class ChatSyncConnectionExistsError extends Schema.TaggedError()( +export class ChatSyncIntegrationNotConnectedError extends Schema.TaggedErrorClass()( "ChatSyncIntegrationNotConnectedError", { organizationId: OrganizationId, @@ -75,7 +75,7 @@ export class ChatSyncIntegrationNotConnectedError extends Schema.TaggedError()( +export class ChatSyncChannelLinkExistsError extends Schema.TaggedErrorClass()( "ChatSyncChannelLinkExistsError", { syncConnectionId: SyncConnectionId, diff --git a/packages/domain/src/http/incoming-webhooks.ts b/packages/domain/src/http/incoming-webhooks.ts index 1803e0854..4045615f9 100644 --- a/packages/domain/src/http/incoming-webhooks.ts +++ b/packages/domain/src/http/incoming-webhooks.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { ChannelWebhookId } from "@hazel/schema" import { Schema } from "effect" import { InternalServerError } from "../errors" @@ -100,30 +100,30 @@ export class WebhookMessageResponse extends Schema.Class }) {} // Error: Webhook not found -export class WebhookNotFoundError extends Schema.TaggedError()( +export class WebhookNotFoundError extends Schema.TaggedErrorClass()( "WebhookNotFoundError", { message: Schema.String, }, - HttpApiSchema.annotations({ status: 404 }), + HttpApiSchema.status(404), ) {} // Error: Webhook is disabled -export class WebhookDisabledError extends Schema.TaggedError()( +export class WebhookDisabledError extends Schema.TaggedErrorClass()( "WebhookDisabledError", { message: Schema.String, }, - HttpApiSchema.annotations({ status: 403 }), + HttpApiSchema.status(403), ) {} // Error: Invalid webhook token -export class InvalidWebhookTokenError extends Schema.TaggedError()( +export class InvalidWebhookTokenError extends Schema.TaggedErrorClass()( "InvalidWebhookTokenError", { message: Schema.String, }, - HttpApiSchema.annotations({ status: 401 }), + HttpApiSchema.status(401), ) {} // Public endpoint - no auth middleware, uses webhook token in URL diff --git a/packages/domain/src/http/integration-commands.ts b/packages/domain/src/http/integration-commands.ts index b25529632..11653793c 100644 --- a/packages/domain/src/http/integration-commands.ts +++ b/packages/domain/src/http/integration-commands.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" import * as CurrentUser from "../current-user" import { InternalServerError, UnauthorizedError } from "../errors" diff --git a/packages/domain/src/http/integration-resources.ts b/packages/domain/src/http/integration-resources.ts index 00d527edb..863db73e9 100644 --- a/packages/domain/src/http/integration-resources.ts +++ b/packages/domain/src/http/integration-resources.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" import * as CurrentUser from "../current-user" import { InternalServerError, UnauthorizedError } from "../errors" @@ -131,7 +131,7 @@ export class DiscordGuildChannelsResponse extends Schema.Class()( +export class IntegrationNotConnectedForPreviewError extends Schema.TaggedErrorClass()( "IntegrationNotConnectedForPreviewError", { provider: IntegrationProvider, @@ -139,7 +139,7 @@ export class IntegrationNotConnectedForPreviewError extends Schema.TaggedError()( +export class ResourceNotFoundError extends Schema.TaggedErrorClass()( "ResourceNotFoundError", { url: Schema.String, @@ -148,7 +148,7 @@ export class ResourceNotFoundError extends Schema.TaggedError()( +export class IntegrationResourceError extends Schema.TaggedErrorClass()( "IntegrationResourceError", { url: Schema.String, diff --git a/packages/domain/src/http/integrations.ts b/packages/domain/src/http/integrations.ts index fe9fe3733..d7374d74a 100644 --- a/packages/domain/src/http/integrations.ts +++ b/packages/domain/src/http/integrations.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" import * as CurrentUser from "../current-user" import { InternalServerError, UnauthorizedError } from "../errors" @@ -27,28 +27,28 @@ export class ConnectionStatusResponse extends Schema.Class()( +export class IntegrationNotConnectedError extends Schema.TaggedErrorClass()( "IntegrationNotConnectedError", { provider: IntegrationProvider, }, ) {} -export class InvalidOAuthStateError extends Schema.TaggedError()( +export class InvalidOAuthStateError extends Schema.TaggedErrorClass()( "InvalidOAuthStateError", { message: Schema.String, }, ) {} -export class UnsupportedProviderError extends Schema.TaggedError()( +export class UnsupportedProviderError extends Schema.TaggedErrorClass()( "UnsupportedProviderError", { provider: Schema.String, }, ) {} -export class InvalidApiKeyError extends Schema.TaggedError()("InvalidApiKeyError", { +export class InvalidApiKeyError extends Schema.TaggedErrorClass()("InvalidApiKeyError", { message: Schema.String, }) {} diff --git a/packages/domain/src/http/internal.ts b/packages/domain/src/http/internal.ts index 56992aae3..09b548a89 100644 --- a/packages/domain/src/http/internal.ts +++ b/packages/domain/src/http/internal.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { InvalidBearerTokenError } from "../session-errors" diff --git a/packages/domain/src/http/klipy.ts b/packages/domain/src/http/klipy.ts index 5c3d1db3e..a2544c464 100644 --- a/packages/domain/src/http/klipy.ts +++ b/packages/domain/src/http/klipy.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi" import { Schema } from "effect" import { CurrentUser } from "../" import { RequiredScopes } from "../scopes/required-scopes" @@ -57,14 +57,12 @@ export class KlipyCategoriesResponse extends Schema.Class("KlipyApiError")( +export class KlipyApiError extends Schema.TaggedErrorClass("KlipyApiError")( "KlipyApiError", { message: Schema.String, }, - HttpApiSchema.annotations({ - status: 502, - }), + HttpApiSchema.status(502), ) {} // ============ API Group ============ diff --git a/packages/domain/src/http/mock-data.ts b/packages/domain/src/http/mock-data.ts index 10a3265cc..a16af43aa 100644 --- a/packages/domain/src/http/mock-data.ts +++ b/packages/domain/src/http/mock-data.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" import * as CurrentUser from "../current-user.ts" import { InternalServerError, UnauthorizedError } from "../errors.ts" diff --git a/packages/domain/src/http/presence.ts b/packages/domain/src/http/presence.ts index fd15a31aa..75196f3c0 100644 --- a/packages/domain/src/http/presence.ts +++ b/packages/domain/src/http/presence.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" import { InternalServerError } from "../errors" import { UserId } from "@hazel/schema" diff --git a/packages/domain/src/http/root.ts b/packages/domain/src/http/root.ts index 6e5ab5f88..8fd6b8e5e 100644 --- a/packages/domain/src/http/root.ts +++ b/packages/domain/src/http/root.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" import { Schema } from "effect" import { RequiredScopes } from "../scopes/required-scopes" diff --git a/packages/domain/src/http/uploads.ts b/packages/domain/src/http/uploads.ts index c893a2345..1126607c5 100644 --- a/packages/domain/src/http/uploads.ts +++ b/packages/domain/src/http/uploads.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi" import { Schema } from "effect" import { CurrentUser, InternalServerError, UnauthorizedError } from "../" import { AttachmentId, BotId, ChannelId, OrganizationId } from "@hazel/schema" @@ -161,38 +161,32 @@ export class PresignUploadResponse extends Schema.Class(" // ============ Error Schemas ============ -export class UploadError extends Schema.TaggedError("UploadError")( +export class UploadError extends Schema.TaggedErrorClass("UploadError")( "UploadError", { message: Schema.String, }, - HttpApiSchema.annotations({ - status: 500, - }), + HttpApiSchema.status(500), ) {} -export class BotNotFoundForUploadError extends Schema.TaggedError( +export class BotNotFoundForUploadError extends Schema.TaggedErrorClass( "BotNotFoundForUploadError", )( "BotNotFoundForUploadError", { botId: BotId, }, - HttpApiSchema.annotations({ - status: 404, - }), + HttpApiSchema.status(404), ) {} -export class OrganizationNotFoundForUploadError extends Schema.TaggedError( +export class OrganizationNotFoundForUploadError extends Schema.TaggedErrorClass( "OrganizationNotFoundForUploadError", )( "OrganizationNotFoundForUploadError", { organizationId: OrganizationId, }, - HttpApiSchema.annotations({ - status: 404, - }), + HttpApiSchema.status(404), ) {} // ============ API Group ============ diff --git a/packages/domain/src/http/webhooks.ts b/packages/domain/src/http/webhooks.ts index 69b47796a..9842f53b5 100644 --- a/packages/domain/src/http/webhooks.ts +++ b/packages/domain/src/http/webhooks.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "@effect/platform" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" import { InternalServerError, WorkflowInitializationError } from "../errors" import { RequiredScopes } from "../scopes/required-scopes" @@ -16,16 +16,14 @@ export class WebhookResponse extends Schema.Class("WebhookRespo message: Schema.optional(Schema.String), }) {} -export class InvalidWebhookSignature extends Schema.TaggedError( +export class InvalidWebhookSignature extends Schema.TaggedErrorClass( "InvalidWebhookSignature", )( "InvalidWebhookSignature", { message: Schema.String, }, - HttpApiSchema.annotations({ - status: 401, - }), + HttpApiSchema.status(401), ) {} // GitHub Webhook Types @@ -34,16 +32,14 @@ export class GitHubWebhookResponse extends Schema.Class(" messagesCreated: Schema.optional(Schema.Number), }) {} -export class InvalidGitHubWebhookSignature extends Schema.TaggedError( +export class InvalidGitHubWebhookSignature extends Schema.TaggedErrorClass( "InvalidGitHubWebhookSignature", )( "InvalidGitHubWebhookSignature", { message: Schema.String, }, - HttpApiSchema.annotations({ - status: 401, - }), + HttpApiSchema.status(401), ) {} export class WebhookGroup extends HttpApiGroup.make("webhooks") diff --git a/packages/domain/src/rate-limit-errors.ts b/packages/domain/src/rate-limit-errors.ts index 8cd202b31..928813eeb 100644 --- a/packages/domain/src/rate-limit-errors.ts +++ b/packages/domain/src/rate-limit-errors.ts @@ -1,11 +1,11 @@ -import { HttpApiSchema } from "@effect/platform" +import { HttpApiSchema } from "effect/unstable/httpapi" import { Schema } from "effect" /** * Error thrown when a user exceeds their rate limit. * Contains information about when they can retry. */ -export class RateLimitExceededError extends Schema.TaggedError()( +export class RateLimitExceededError extends Schema.TaggedErrorClass()( "RateLimitExceededError", { message: Schema.String, @@ -13,7 +13,5 @@ export class RateLimitExceededError extends Schema.TaggedError()( +export class AttachmentNotFoundError extends Schema.TaggedErrorClass()( "AttachmentNotFoundError", { attachmentId: AttachmentId, diff --git a/packages/domain/src/rpc/bots.ts b/packages/domain/src/rpc/bots.ts index 1bef2f2eb..e6ce7bf4c 100644 --- a/packages/domain/src/rpc/bots.ts +++ b/packages/domain/src/rpc/bots.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { BotId } from "@hazel/schema" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" @@ -61,14 +61,14 @@ export class PublicBotListResponse extends Schema.Class(" /** * Error thrown when a bot is not found. */ -export class BotNotFoundError extends Schema.TaggedError()("BotNotFoundError", { +export class BotNotFoundError extends Schema.TaggedErrorClass()("BotNotFoundError", { botId: BotId, }) {} /** * Error thrown when a bot is already installed in the organization. */ -export class BotAlreadyInstalledError extends Schema.TaggedError()( +export class BotAlreadyInstalledError extends Schema.TaggedErrorClass()( "BotAlreadyInstalledError", { botId: BotId, diff --git a/packages/domain/src/rpc/channel-members.ts b/packages/domain/src/rpc/channel-members.ts index ba22ca831..5f5e3410b 100644 --- a/packages/domain/src/rpc/channel-members.ts +++ b/packages/domain/src/rpc/channel-members.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { ChannelId, ChannelMemberId } from "@hazel/schema" @@ -21,7 +21,7 @@ export class ChannelMemberResponse extends Schema.Class(" * Error thrown when a channel member is not found. * Used in update and delete operations. */ -export class ChannelMemberNotFoundError extends Schema.TaggedError()( +export class ChannelMemberNotFoundError extends Schema.TaggedErrorClass()( "ChannelMemberNotFoundError", { channelMemberId: ChannelMemberId, diff --git a/packages/domain/src/rpc/channel-sections.ts b/packages/domain/src/rpc/channel-sections.ts index 4e9d82ba5..96371105f 100644 --- a/packages/domain/src/rpc/channel-sections.ts +++ b/packages/domain/src/rpc/channel-sections.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { ChannelId, ChannelSectionId, OrganizationId } from "@hazel/schema" @@ -21,7 +21,7 @@ export class ChannelSectionResponse extends Schema.Class * Error thrown when a channel section is not found. * Used in update and delete operations. */ -export class ChannelSectionNotFoundError extends Schema.TaggedError()( +export class ChannelSectionNotFoundError extends Schema.TaggedErrorClass()( "ChannelSectionNotFoundError", { sectionId: ChannelSectionId, diff --git a/packages/domain/src/rpc/channel-webhooks.ts b/packages/domain/src/rpc/channel-webhooks.ts index 17def7c4c..f8dcb179d 100644 --- a/packages/domain/src/rpc/channel-webhooks.ts +++ b/packages/domain/src/rpc/channel-webhooks.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { AvatarUrl, ChannelId, ChannelWebhookId } from "@hazel/schema" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" @@ -41,7 +41,7 @@ export class ChannelWebhookListResponse extends Schema.Class()( +export class ChannelWebhookNotFoundError extends Schema.TaggedErrorClass()( "ChannelWebhookNotFoundError", { webhookId: ChannelWebhookId, diff --git a/packages/domain/src/rpc/channels.ts b/packages/domain/src/rpc/channels.ts index 8d193c270..6a0b27240 100644 --- a/packages/domain/src/rpc/channels.ts +++ b/packages/domain/src/rpc/channels.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { AIProviderUnavailableError, @@ -36,7 +36,7 @@ export class ChannelResponse extends Schema.Class("ChannelRespo * Error thrown when a channel is not found. * Used in update and delete operations. */ -export class ChannelNotFoundError extends Schema.TaggedError()("ChannelNotFoundError", { +export class ChannelNotFoundError extends Schema.TaggedErrorClass()("ChannelNotFoundError", { channelId: ChannelId, }) {} diff --git a/packages/domain/src/rpc/chat-sync.ts b/packages/domain/src/rpc/chat-sync.ts index 6be6d62ce..bf4326046 100644 --- a/packages/domain/src/rpc/chat-sync.ts +++ b/packages/domain/src/rpc/chat-sync.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { ChannelId, ExternalChannelId, @@ -40,21 +40,21 @@ export class ChatSyncChannelLinkListResponse extends Schema.Class()( +export class ChatSyncConnectionNotFoundError extends Schema.TaggedErrorClass()( "ChatSyncConnectionNotFoundError", { syncConnectionId: SyncConnectionId, }, ) {} -export class ChatSyncChannelLinkNotFoundError extends Schema.TaggedError()( +export class ChatSyncChannelLinkNotFoundError extends Schema.TaggedErrorClass()( "ChatSyncChannelLinkNotFoundError", { syncChannelLinkId: SyncChannelLinkId, }, ) {} -export class ChatSyncConnectionExistsError extends Schema.TaggedError()( +export class ChatSyncConnectionExistsError extends Schema.TaggedErrorClass()( "ChatSyncConnectionExistsError", { organizationId: OrganizationId, @@ -63,7 +63,7 @@ export class ChatSyncConnectionExistsError extends Schema.TaggedError()( +export class ChatSyncIntegrationNotConnectedError extends Schema.TaggedErrorClass()( "ChatSyncIntegrationNotConnectedError", { organizationId: OrganizationId, @@ -71,7 +71,7 @@ export class ChatSyncIntegrationNotConnectedError extends Schema.TaggedError()( +export class ChatSyncChannelLinkExistsError extends Schema.TaggedErrorClass()( "ChatSyncChannelLinkExistsError", { syncConnectionId: SyncConnectionId, diff --git a/packages/domain/src/rpc/connect-shares.ts b/packages/domain/src/rpc/connect-shares.ts index d52a4ede3..89f45bb53 100644 --- a/packages/domain/src/rpc/connect-shares.ts +++ b/packages/domain/src/rpc/connect-shares.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { ChannelId, @@ -53,7 +53,7 @@ export class ConnectWorkspaceSearchResponse extends Schema.Class()( +export class ConnectInviteNotFoundError extends Schema.TaggedErrorClass()( "ConnectInviteNotFoundError", { inviteId: ConnectInviteId, @@ -61,7 +61,7 @@ export class ConnectInviteNotFoundError extends Schema.TaggedError()( +export class ConnectInviteInvalidStateError extends Schema.TaggedErrorClass()( "ConnectInviteInvalidStateError", { inviteId: ConnectInviteId, @@ -70,14 +70,14 @@ export class ConnectInviteInvalidStateError extends Schema.TaggedError()( +export class ConnectWorkspaceNotFoundError extends Schema.TaggedErrorClass()( "ConnectWorkspaceNotFoundError", { message: Schema.String, }, ) {} -export class ConnectChannelAlreadySharedError extends Schema.TaggedError()( +export class ConnectChannelAlreadySharedError extends Schema.TaggedErrorClass()( "ConnectChannelAlreadySharedError", { channelId: ChannelId, diff --git a/packages/domain/src/rpc/custom-emojis.ts b/packages/domain/src/rpc/custom-emojis.ts index b50346085..8086e4bb4 100644 --- a/packages/domain/src/rpc/custom-emojis.ts +++ b/packages/domain/src/rpc/custom-emojis.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { CustomEmojiId, OrganizationId, TransactionId } from "@hazel/schema" @@ -18,7 +18,7 @@ export class CustomEmojiResponse extends Schema.Class("Cust /** * Error thrown when a custom emoji is not found. */ -export class CustomEmojiNotFoundError extends Schema.TaggedError()( +export class CustomEmojiNotFoundError extends Schema.TaggedErrorClass()( "CustomEmojiNotFoundError", { customEmojiId: CustomEmojiId, @@ -28,7 +28,7 @@ export class CustomEmojiNotFoundError extends Schema.TaggedError()( +export class CustomEmojiNameConflictError extends Schema.TaggedErrorClass()( "CustomEmojiNameConflictError", { name: Schema.String, @@ -40,7 +40,7 @@ export class CustomEmojiNameConflictError extends Schema.TaggedError()( +export class CustomEmojiDeletedExistsError extends Schema.TaggedErrorClass()( "CustomEmojiDeletedExistsError", { customEmojiId: CustomEmojiId, diff --git a/packages/domain/src/rpc/github-subscriptions.ts b/packages/domain/src/rpc/github-subscriptions.ts index c8816d840..39dde82c3 100644 --- a/packages/domain/src/rpc/github-subscriptions.ts +++ b/packages/domain/src/rpc/github-subscriptions.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { ChannelId, GitHubSubscriptionId } from "@hazel/schema" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" @@ -31,7 +31,7 @@ export class GitHubSubscriptionListResponse extends Schema.Class()( +export class GitHubSubscriptionNotFoundError extends Schema.TaggedErrorClass()( "GitHubSubscriptionNotFoundError", { subscriptionId: GitHubSubscriptionId, @@ -41,7 +41,7 @@ export class GitHubSubscriptionNotFoundError extends Schema.TaggedError()( +export class GitHubSubscriptionExistsError extends Schema.TaggedErrorClass()( "GitHubSubscriptionExistsError", { channelId: ChannelId, @@ -52,7 +52,7 @@ export class GitHubSubscriptionExistsError extends Schema.TaggedError()( +export class GitHubNotConnectedError extends Schema.TaggedErrorClass()( "GitHubNotConnectedError", {}, ) {} diff --git a/packages/domain/src/rpc/integration-requests.ts b/packages/domain/src/rpc/integration-requests.ts index 7bfd4d3d5..3a0c24b3a 100644 --- a/packages/domain/src/rpc/integration-requests.ts +++ b/packages/domain/src/rpc/integration-requests.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { IntegrationRequestId, OrganizationId } from "@hazel/schema" diff --git a/packages/domain/src/rpc/invitations.ts b/packages/domain/src/rpc/invitations.ts index 117c0dabf..a03e74ae2 100644 --- a/packages/domain/src/rpc/invitations.ts +++ b/packages/domain/src/rpc/invitations.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { InvitationId, OrganizationId } from "@hazel/schema" @@ -44,7 +44,7 @@ export class InvitationBatchResponse extends Schema.Class()( +export class InvitationNotFoundError extends Schema.TaggedErrorClass()( "InvitationNotFoundError", { invitationId: InvitationId, diff --git a/packages/domain/src/rpc/message-reactions.ts b/packages/domain/src/rpc/message-reactions.ts index fdf2416e2..560bacf0e 100644 --- a/packages/domain/src/rpc/message-reactions.ts +++ b/packages/domain/src/rpc/message-reactions.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { MessageReactionId } from "@hazel/schema" @@ -23,7 +23,7 @@ export class MessageReactionResponse extends Schema.Class()( +export class MessageReactionNotFoundError extends Schema.TaggedErrorClass()( "MessageReactionNotFoundError", { messageReactionId: MessageReactionId, diff --git a/packages/domain/src/rpc/messages.ts b/packages/domain/src/rpc/messages.ts index 345e55b23..27c82ffec 100644 --- a/packages/domain/src/rpc/messages.ts +++ b/packages/domain/src/rpc/messages.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, MessageNotFoundError, UnauthorizedError } from "../errors" import { MessageId } from "@hazel/schema" diff --git a/packages/domain/src/rpc/middleware.ts b/packages/domain/src/rpc/middleware.ts index d9c934d29..09986d07d 100644 --- a/packages/domain/src/rpc/middleware.ts +++ b/packages/domain/src/rpc/middleware.ts @@ -5,7 +5,7 @@ * in browser code. Server-side implementations live in the backend package. */ -import { RpcMiddleware } from "@effect/rpc" +import { RpcMiddleware } from "effect/unstable/rpc" import { Schema as S } from "effect" import * as CurrentUser from "../current-user" import { UnauthorizedError } from "../errors" diff --git a/packages/domain/src/rpc/notifications.ts b/packages/domain/src/rpc/notifications.ts index fa968aa0f..ae87022bb 100644 --- a/packages/domain/src/rpc/notifications.ts +++ b/packages/domain/src/rpc/notifications.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { ChannelId, MessageId, NotificationId } from "@hazel/schema" @@ -20,7 +20,7 @@ export class NotificationResponse extends Schema.Class("No * Error thrown when a notification is not found. * Used in update and delete operations. */ -export class NotificationNotFoundError extends Schema.TaggedError()( +export class NotificationNotFoundError extends Schema.TaggedErrorClass()( "NotificationNotFoundError", { notificationId: NotificationId, diff --git a/packages/domain/src/rpc/organization-members.ts b/packages/domain/src/rpc/organization-members.ts index 35c698b2c..10821a717 100644 --- a/packages/domain/src/rpc/organization-members.ts +++ b/packages/domain/src/rpc/organization-members.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { OrganizationMemberId } from "@hazel/schema" @@ -23,7 +23,7 @@ export class OrganizationMemberResponse extends Schema.Class()( +export class OrganizationMemberNotFoundError extends Schema.TaggedErrorClass()( "OrganizationMemberNotFoundError", { organizationMemberId: OrganizationMemberId, diff --git a/packages/domain/src/rpc/organizations.ts b/packages/domain/src/rpc/organizations.ts index 61a6031b3..f834c499d 100644 --- a/packages/domain/src/rpc/organizations.ts +++ b/packages/domain/src/rpc/organizations.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { OrganizationId } from "@hazel/schema" @@ -20,7 +20,7 @@ export class OrganizationResponse extends Schema.Class("Or * Error thrown when an organization is not found. * Used in update and delete operations. */ -export class OrganizationNotFoundError extends Schema.TaggedError()( +export class OrganizationNotFoundError extends Schema.TaggedErrorClass()( "OrganizationNotFoundError", { organizationId: OrganizationId, @@ -30,7 +30,7 @@ export class OrganizationNotFoundError extends Schema.TaggedError()( +export class OrganizationSlugAlreadyExistsError extends Schema.TaggedErrorClass()( "OrganizationSlugAlreadyExistsError", { message: Schema.String, @@ -41,7 +41,7 @@ export class OrganizationSlugAlreadyExistsError extends Schema.TaggedError()( +export class PublicInviteDisabledError extends Schema.TaggedErrorClass()( "PublicInviteDisabledError", { organizationId: OrganizationId, @@ -51,7 +51,7 @@ export class PublicInviteDisabledError extends Schema.TaggedError()("AlreadyMemberError", { +export class AlreadyMemberError extends Schema.TaggedErrorClass()("AlreadyMemberError", { organizationId: OrganizationId, organizationSlug: Schema.NullOr(Schema.String), }) {} diff --git a/packages/domain/src/rpc/pinned-messages.ts b/packages/domain/src/rpc/pinned-messages.ts index f7b91332c..75b98b121 100644 --- a/packages/domain/src/rpc/pinned-messages.ts +++ b/packages/domain/src/rpc/pinned-messages.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { ChannelId, MessageId, PinnedMessageId } from "@hazel/schema" @@ -21,7 +21,7 @@ export class PinnedMessageResponse extends Schema.Class(" * Error thrown when a pinned message is not found. * Used in update and delete operations. */ -export class PinnedMessageNotFoundError extends Schema.TaggedError()( +export class PinnedMessageNotFoundError extends Schema.TaggedErrorClass()( "PinnedMessageNotFoundError", { pinnedMessageId: PinnedMessageId, diff --git a/packages/domain/src/rpc/rss-subscriptions.ts b/packages/domain/src/rpc/rss-subscriptions.ts index bb91234c2..c6db1b7cf 100644 --- a/packages/domain/src/rpc/rss-subscriptions.ts +++ b/packages/domain/src/rpc/rss-subscriptions.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { ChannelId, RssSubscriptionId } from "@hazel/schema" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" @@ -21,14 +21,14 @@ export class RssSubscriptionListResponse extends Schema.Class()( +export class RssSubscriptionNotFoundError extends Schema.TaggedErrorClass()( "RssSubscriptionNotFoundError", { subscriptionId: RssSubscriptionId, }, ) {} -export class RssSubscriptionExistsError extends Schema.TaggedError()( +export class RssSubscriptionExistsError extends Schema.TaggedErrorClass()( "RssSubscriptionExistsError", { channelId: ChannelId, @@ -36,7 +36,7 @@ export class RssSubscriptionExistsError extends Schema.TaggedError()( +export class RssFeedValidationError extends Schema.TaggedErrorClass()( "RssFeedValidationError", { feedUrl: Schema.String, diff --git a/packages/domain/src/rpc/scope-injection-middleware.ts b/packages/domain/src/rpc/scope-injection-middleware.ts index ab831e0d1..bf0f6a586 100644 --- a/packages/domain/src/rpc/scope-injection-middleware.ts +++ b/packages/domain/src/rpc/scope-injection-middleware.ts @@ -1,4 +1,4 @@ -import { RpcMiddleware } from "@effect/rpc" +import { RpcMiddleware } from "effect/unstable/rpc" /** * Middleware that reads RequiredScopes from the RPC annotation diff --git a/packages/domain/src/rpc/typing-indicators.ts b/packages/domain/src/rpc/typing-indicators.ts index d2a348044..79bf908cf 100644 --- a/packages/domain/src/rpc/typing-indicators.ts +++ b/packages/domain/src/rpc/typing-indicators.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { TypingIndicatorId } from "@hazel/schema" @@ -22,7 +22,7 @@ export class TypingIndicatorResponse extends Schema.Class()( +export class TypingIndicatorNotFoundError extends Schema.TaggedErrorClass()( "TypingIndicatorNotFoundError", { typingIndicatorId: TypingIndicatorId, diff --git a/packages/domain/src/rpc/user-presence-status.ts b/packages/domain/src/rpc/user-presence-status.ts index 4bb97ea0b..e4181930c 100644 --- a/packages/domain/src/rpc/user-presence-status.ts +++ b/packages/domain/src/rpc/user-presence-status.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import { InternalServerError, UnauthorizedError } from "../errors" import { UserPresenceStatusId } from "@hazel/schema" @@ -23,7 +23,7 @@ export class UserPresenceStatusResponse extends Schema.Class()( +export class UserPresenceStatusNotFoundError extends Schema.TaggedErrorClass()( "UserPresenceStatusNotFoundError", { statusId: UserPresenceStatusId, diff --git a/packages/domain/src/rpc/users.ts b/packages/domain/src/rpc/users.ts index 630ef407b..5fd654ab5 100644 --- a/packages/domain/src/rpc/users.ts +++ b/packages/domain/src/rpc/users.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from "@effect/rpc" +import { Rpc, RpcGroup } from "effect/unstable/rpc" import { Schema } from "effect" import * as CurrentUser from "../current-user" import { InternalServerError, UnauthorizedError } from "../errors" @@ -21,7 +21,7 @@ export class UserResponse extends Schema.Class("UserResponse")({ * Error thrown when a user is not found. * Used in update and delete operations. */ -export class UserNotFoundError extends Schema.TaggedError()("UserNotFoundError", { +export class UserNotFoundError extends Schema.TaggedErrorClass()("UserNotFoundError", { userId: UserId, }) {} diff --git a/packages/domain/src/scopes/permission-error.ts b/packages/domain/src/scopes/permission-error.ts index 3d32990fb..d78d6e4a7 100644 --- a/packages/domain/src/scopes/permission-error.ts +++ b/packages/domain/src/scopes/permission-error.ts @@ -1,16 +1,14 @@ -import { HttpApiSchema } from "@effect/platform" +import { HttpApiSchema } from "effect/unstable/httpapi" import { Predicate, Schema } from "effect" import type { ApiScope } from "./api-scope" -export class PermissionError extends Schema.TaggedError("PermissionError")( +export class PermissionError extends Schema.TaggedErrorClass("PermissionError")( "PermissionError", { message: Schema.String, requiredScope: Schema.optional(Schema.String), }, - HttpApiSchema.annotations({ - status: 403, - }), + HttpApiSchema.status(403), ) { static is(u: unknown): u is PermissionError { return Predicate.isTagged(u, "PermissionError") diff --git a/packages/domain/src/scopes/required-scopes.ts b/packages/domain/src/scopes/required-scopes.ts index dff7ebd58..6b4c1c60c 100644 --- a/packages/domain/src/scopes/required-scopes.ts +++ b/packages/domain/src/scopes/required-scopes.ts @@ -1,4 +1,4 @@ -import { Context } from "effect" +import { ServiceMap } from "effect" import type { ApiScope } from "./api-scope" /** @@ -14,7 +14,6 @@ import type { ApiScope } from "./api-scope" * - Empty array `[]` = public endpoint (no scope needed) * - Missing annotation = error (caught by startup validation) */ -export class RequiredScopes extends Context.Tag("@hazel/domain/RequiredScopes")< - RequiredScopes, +export class RequiredScopes extends ServiceMap.Service ->() {} +>()("@hazel/domain/RequiredScopes") {} diff --git a/packages/domain/src/session-errors.ts b/packages/domain/src/session-errors.ts index c60b0eac7..ef3b4853f 100644 --- a/packages/domain/src/session-errors.ts +++ b/packages/domain/src/session-errors.ts @@ -1,8 +1,8 @@ -import { HttpApiSchema } from "@effect/platform" +import { HttpApiSchema } from "effect/unstable/httpapi" import { Schema } from "effect" // 401 Errors - Client needs to re-authenticate -export class SessionNotProvidedError extends Schema.TaggedError( +export class SessionNotProvidedError extends Schema.TaggedErrorClass( "SessionNotProvidedError", )( "SessionNotProvidedError", @@ -10,12 +10,10 @@ export class SessionNotProvidedError extends Schema.TaggedError( +export class SessionAuthenticationError extends Schema.TaggedErrorClass( "SessionAuthenticationError", )( "SessionAuthenticationError", @@ -23,12 +21,10 @@ export class SessionAuthenticationError extends Schema.TaggedError( +export class InvalidJwtPayloadError extends Schema.TaggedErrorClass( "InvalidJwtPayloadError", )( "InvalidJwtPayloadError", @@ -36,23 +32,19 @@ export class InvalidJwtPayloadError extends Schema.TaggedError("SessionExpiredError")( +export class SessionExpiredError extends Schema.TaggedErrorClass("SessionExpiredError")( "SessionExpiredError", { message: Schema.String, detail: Schema.String, }, - HttpApiSchema.annotations({ - status: 401, - }), + HttpApiSchema.status(401), ) {} -export class InvalidBearerTokenError extends Schema.TaggedError( +export class InvalidBearerTokenError extends Schema.TaggedErrorClass( "InvalidBearerTokenError", )( "InvalidBearerTokenError", @@ -60,41 +52,33 @@ export class InvalidBearerTokenError extends Schema.TaggedError("SessionLoadError")( +export class SessionLoadError extends Schema.TaggedErrorClass("SessionLoadError")( "SessionLoadError", { message: Schema.String, detail: Schema.String, }, - HttpApiSchema.annotations({ - status: 503, - }), + HttpApiSchema.status(503), ) {} -export class SessionRefreshError extends Schema.TaggedError("SessionRefreshError")( +export class SessionRefreshError extends Schema.TaggedErrorClass("SessionRefreshError")( "SessionRefreshError", { message: Schema.String, detail: Schema.String, }, - HttpApiSchema.annotations({ - status: 401, - }), + HttpApiSchema.status(401), ) {} -export class WorkOSUserFetchError extends Schema.TaggedError("WorkOSUserFetchError")( +export class WorkOSUserFetchError extends Schema.TaggedErrorClass("WorkOSUserFetchError")( "WorkOSUserFetchError", { message: Schema.String, detail: Schema.String, }, - HttpApiSchema.annotations({ - status: 503, - }), + HttpApiSchema.status(503), ) {} diff --git a/packages/effect-bun/package.json b/packages/effect-bun/package.json index a7ac8cd6e..f35e11a3b 100644 --- a/packages/effect-bun/package.json +++ b/packages/effect-bun/package.json @@ -15,14 +15,11 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@effect/experimental": "catalog:effect", "@effect/opentelemetry": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-bun": "catalog:effect", "effect": "catalog:effect" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "latest", "typescript": "^5.9.3" } diff --git a/packages/effect-bun/src/Redis.ts b/packages/effect-bun/src/Redis.ts index f9f9057e2..5a3aae2e9 100644 --- a/packages/effect-bun/src/Redis.ts +++ b/packages/effect-bun/src/Redis.ts @@ -6,7 +6,7 @@ import { Config, Context, Duration, Effect, Layer, Match, Schema } from "effect" /** * Base Redis error - used for unknown error codes */ -export class RedisError extends Schema.TaggedError()("RedisError", { +export class RedisError extends Schema.TaggedErrorClass()("RedisError", { message: Schema.String, code: Schema.optional(Schema.String), cause: Schema.optional(Schema.Unknown), @@ -16,7 +16,7 @@ export class RedisError extends Schema.TaggedError()("RedisError", { * Connection to Redis server was closed * Bun error code: ERR_REDIS_CONNECTION_CLOSED */ -export class RedisConnectionClosedError extends Schema.TaggedError()( +export class RedisConnectionClosedError extends Schema.TaggedErrorClass()( "RedisConnectionClosedError", { message: Schema.String, @@ -27,7 +27,7 @@ export class RedisConnectionClosedError extends Schema.TaggedError()( +export class RedisAuthenticationError extends Schema.TaggedErrorClass()( "RedisAuthenticationError", { message: Schema.String, @@ -38,7 +38,7 @@ export class RedisAuthenticationError extends Schema.TaggedError()( +export class RedisInvalidResponseError extends Schema.TaggedErrorClass()( "RedisInvalidResponseError", { message: Schema.String, diff --git a/packages/effect-bun/src/S3.ts b/packages/effect-bun/src/S3.ts index 22ecfb17a..8fcfcda16 100644 --- a/packages/effect-bun/src/S3.ts +++ b/packages/effect-bun/src/S3.ts @@ -7,7 +7,7 @@ import { Context, Effect, Layer, Match, Schema } from "effect" /** * Base S3 error - used for S3 server errors and unknown error codes */ -export class S3Error extends Schema.TaggedError()("S3Error", { +export class S3Error extends Schema.TaggedErrorClass()("S3Error", { message: Schema.String, code: Schema.optional(Schema.String), cause: Schema.optional(Schema.Unknown), @@ -17,7 +17,7 @@ export class S3Error extends Schema.TaggedError()("S3Error", { * Missing S3 credentials (access key or secret) * Bun error code: ERR_S3_MISSING_CREDENTIALS */ -export class S3MissingCredentialsError extends Schema.TaggedError()( +export class S3MissingCredentialsError extends Schema.TaggedErrorClass()( "S3MissingCredentialsError", { message: Schema.String, @@ -28,7 +28,7 @@ export class S3MissingCredentialsError extends Schema.TaggedError()("S3InvalidMethodError", { +export class S3InvalidMethodError extends Schema.TaggedErrorClass()("S3InvalidMethodError", { message: Schema.String, }) {} @@ -36,7 +36,7 @@ export class S3InvalidMethodError extends Schema.TaggedError()("S3InvalidPathError", { +export class S3InvalidPathError extends Schema.TaggedErrorClass()("S3InvalidPathError", { message: Schema.String, }) {} @@ -44,7 +44,7 @@ export class S3InvalidPathError extends Schema.TaggedError() * Invalid S3 endpoint URL * Bun error code: ERR_S3_INVALID_ENDPOINT */ -export class S3InvalidEndpointError extends Schema.TaggedError()( +export class S3InvalidEndpointError extends Schema.TaggedErrorClass()( "S3InvalidEndpointError", { message: Schema.String, @@ -55,7 +55,7 @@ export class S3InvalidEndpointError extends Schema.TaggedError()( +export class S3InvalidSignatureError extends Schema.TaggedErrorClass()( "S3InvalidSignatureError", { message: Schema.String, @@ -66,7 +66,7 @@ export class S3InvalidSignatureError extends Schema.TaggedError()( +export class S3InvalidSessionTokenError extends Schema.TaggedErrorClass()( "S3InvalidSessionTokenError", { message: Schema.String, diff --git a/packages/effect-bun/src/persistence/redis-backing.ts b/packages/effect-bun/src/persistence/redis-backing.ts index 18d6f4476..68bead25c 100644 --- a/packages/effect-bun/src/persistence/redis-backing.ts +++ b/packages/effect-bun/src/persistence/redis-backing.ts @@ -1,4 +1,4 @@ -import { Persistence } from "@effect/experimental" +import { Persistence } from "effect/unstable/persistence" import { Duration, Effect, Layer, Option } from "effect" import { identity } from "effect/Function" import { Redis } from "../Redis.js" diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 139ea516c..6fef1b363 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -20,7 +20,6 @@ "./rss": "./src/rss/index.ts" }, "dependencies": { - "@effect/platform": "catalog:effect", "@linear/sdk": "^73.0.0", "effect": "catalog:effect", "he": "^1.2.0", @@ -28,7 +27,6 @@ "rss-parser": "^3.13.0" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "@types/he": "^1.2.3", "typescript": "^5.9.3" diff --git a/packages/integrations/src/craft/api-client.ts b/packages/integrations/src/craft/api-client.ts index 16bd1b8d9..bbbf25713 100644 --- a/packages/integrations/src/craft/api-client.ts +++ b/packages/integrations/src/craft/api-client.ts @@ -8,8 +8,8 @@ * and Bearer tokens instead of a single OAuth endpoint. */ -import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "@effect/platform" -import { Duration, Effect, Schedule, Schema } from "effect" +import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { ServiceMap, Duration, Effect, Schedule, Schema } from "effect" // ============================================================================ // Configuration @@ -116,18 +116,18 @@ export type CraftSpaceInfo = typeof CraftSpaceInfo.Type // Error Types // ============================================================================ -export class CraftApiError extends Schema.TaggedError()("CraftApiError", { +export class CraftApiError extends Schema.TaggedErrorClass()("CraftApiError", { message: Schema.String, status: Schema.optional(Schema.Number), cause: Schema.optional(Schema.Unknown), }) {} -export class CraftNotFoundError extends Schema.TaggedError()("CraftNotFoundError", { +export class CraftNotFoundError extends Schema.TaggedErrorClass()("CraftNotFoundError", { resourceType: Schema.String, resourceId: Schema.String, }) {} -export class CraftRateLimitError extends Schema.TaggedError()("CraftRateLimitError", { +export class CraftRateLimitError extends Schema.TaggedErrorClass()("CraftRateLimitError", { message: Schema.String, retryAfter: Schema.optional(Schema.Number), }) {} @@ -224,9 +224,8 @@ const isRetryableError = (error: CraftApiError | CraftNotFoundError | CraftRateL * const documents = yield* CraftApiClient.listDocuments(baseUrl, accessToken) * ``` */ -export class CraftApiClient extends Effect.Service()("CraftApiClient", { - accessors: true, - effect: Effect.gen(function* () { +export class CraftApiClient extends ServiceMap.Service()("CraftApiClient", { + make: Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient /** @@ -368,7 +367,7 @@ export class CraftApiClient extends Effect.Service()("CraftApiCl const raw = yield* executeRequest(client, "GET", path) const normalizedBlocks = normalizeCraftItemsResponse(raw) return yield* Schema.decodeUnknown(Schema.Array(CraftBlock))(normalizedBlocks).pipe( - Effect.catchAll(() => + Effect.catch(() => Effect.succeed( normalizedBlocks.length > 0 ? (normalizedBlocks as CraftBlock[]) @@ -496,7 +495,7 @@ export class CraftApiClient extends Effect.Service()("CraftApiCl const raw = yield* executeRequest(client, "GET", path) const normalizedDocuments = normalizeCraftItemsResponse(raw) return yield* Schema.decodeUnknown(Schema.Array(CraftDocument))(normalizedDocuments).pipe( - Effect.catchAll(() => Effect.succeed(normalizedDocuments as CraftDocument[])), + Effect.catch(() => Effect.succeed(normalizedDocuments as CraftDocument[])), ) }).pipe( Effect.retry({ schedule: makeRetrySchedule, while: isRetryableError }), @@ -598,7 +597,7 @@ export class CraftApiClient extends Effect.Service()("CraftApiCl const raw = yield* executeRequest(client, "GET", "/folders") const normalizedFolders = normalizeCraftItemsResponse(raw) return yield* Schema.decodeUnknown(Schema.Array(CraftFolder))(normalizedFolders).pipe( - Effect.catchAll(() => Effect.succeed(normalizedFolders as CraftFolder[])), + Effect.catch(() => Effect.succeed(normalizedFolders as CraftFolder[])), ) }).pipe( Effect.retry({ schedule: makeRetrySchedule, while: isRetryableError }), @@ -667,7 +666,7 @@ export class CraftApiClient extends Effect.Service()("CraftApiCl const raw = yield* executeRequest(client, "GET", path) const normalizedTasks = normalizeCraftItemsResponse(raw) return yield* Schema.decodeUnknown(Schema.Array(CraftTask))(normalizedTasks).pipe( - Effect.catchAll(() => Effect.succeed(normalizedTasks as CraftTask[])), + Effect.catch(() => Effect.succeed(normalizedTasks as CraftTask[])), ) }).pipe( Effect.retry({ schedule: makeRetrySchedule, while: isRetryableError }), @@ -737,7 +736,7 @@ export class CraftApiClient extends Effect.Service()("CraftApiCl const raw = yield* executeRequest(client, "GET", "/collections") const normalizedCollections = normalizeCraftItemsResponse(raw) return yield* Schema.decodeUnknown(Schema.Array(CraftCollection))(normalizedCollections).pipe( - Effect.catchAll(() => Effect.succeed(normalizedCollections as CraftCollection[])), + Effect.catch(() => Effect.succeed(normalizedCollections as CraftCollection[])), ) }).pipe( Effect.retry({ schedule: makeRetrySchedule, while: isRetryableError }), diff --git a/packages/integrations/src/discord/api-client.ts b/packages/integrations/src/discord/api-client.ts index 268abf189..51405b431 100644 --- a/packages/integrations/src/discord/api-client.ts +++ b/packages/integrations/src/discord/api-client.ts @@ -1,5 +1,5 @@ -import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "@effect/platform" -import { Duration, Effect, Schema } from "effect" +import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { ServiceMap, Duration, Effect, Schema } from "effect" export const DiscordAccountInfo = Schema.Struct({ externalAccountId: Schema.String, @@ -62,7 +62,7 @@ const DiscordErrorApiResponse = Schema.Struct({ message: Schema.optionalWith(Schema.String, { default: () => "Unknown Discord error" }), }) -export class DiscordApiError extends Schema.TaggedError()("DiscordApiError", { +export class DiscordApiError extends Schema.TaggedErrorClass()("DiscordApiError", { message: Schema.String, status: Schema.optional(Schema.Number), cause: Schema.optional(Schema.Unknown), @@ -87,9 +87,8 @@ const parseDiscordErrorMessage = (status: number, message: string): string => { const channelTypeIsMessageCapable = (type: number): boolean => type === 0 || type === 5 || type === 10 || type === 11 || type === 12 -export class DiscordApiClient extends Effect.Service()("DiscordApiClient", { - accessors: true, - effect: Effect.gen(function* () { +export class DiscordApiClient extends ServiceMap.Service()("DiscordApiClient", { + make: Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient const makeBearerClient = (token: string) => diff --git a/packages/integrations/src/github/api-client.ts b/packages/integrations/src/github/api-client.ts index 84025ca94..0036f9e92 100644 --- a/packages/integrations/src/github/api-client.ts +++ b/packages/integrations/src/github/api-client.ts @@ -1,5 +1,5 @@ -import { FetchHttpClient, HttpClient, HttpClientRequest } from "@effect/platform" -import { Duration, Effect, Schedule, Schema } from "effect" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { ServiceMap, Duration, Effect, Schedule, Schema } from "effect" /** * GitHub PR URL patterns: @@ -97,14 +97,14 @@ export type GitHubAccountInfo = typeof GitHubAccountInfo.Type // ============================================================================ // Error for when GitHub API request fails -export class GitHubApiError extends Schema.TaggedError()("GitHubApiError", { +export class GitHubApiError extends Schema.TaggedErrorClass()("GitHubApiError", { message: Schema.String, status: Schema.optional(Schema.Number), cause: Schema.optional(Schema.Unknown), }) {} // Error for when PR is not found -export class GitHubPRNotFoundError extends Schema.TaggedError()( +export class GitHubPRNotFoundError extends Schema.TaggedErrorClass()( "GitHubPRNotFoundError", { owner: Schema.String, @@ -114,7 +114,7 @@ export class GitHubPRNotFoundError extends Schema.TaggedError()("GitHubRateLimitError", { +export class GitHubRateLimitError extends Schema.TaggedErrorClass()("GitHubRateLimitError", { message: Schema.String, retryAfter: Schema.optional(Schema.Number), // seconds until rate limit resets }) {} @@ -311,9 +311,8 @@ const isRetryableError = (error: GitHubApiError | GitHubRateLimitError | GitHubP * - Rate limit handling with retry-after header support * - Distributed tracing via Effect spans */ -export class GitHubApiClient extends Effect.Service()("GitHubApiClient", { - accessors: true, - effect: Effect.gen(function* () { +export class GitHubApiClient extends ServiceMap.Service()("GitHubApiClient", { + make: Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient /** @@ -378,7 +377,7 @@ export class GitHubApiClient extends Effect.Service()("GitHubAp if (response.status >= 400) { const errorBody = yield* response.json.pipe( Effect.flatMap(Schema.decodeUnknown(GitHubErrorApiResponse)), - Effect.catchAll((error) => + Effect.catch((error) => Effect.logDebug(`Failed to parse GitHub error response: ${String(error)}`).pipe( Effect.as({ message: "Unknown error" }), ), @@ -463,7 +462,7 @@ export class GitHubApiClient extends Effect.Service()("GitHubAp if (response.status === 403) { const errorBody = yield* response.json.pipe( Effect.flatMap(Schema.decodeUnknown(GitHubErrorApiResponse)), - Effect.catchAll(() => Effect.succeed({ message: "" })), + Effect.catch(() => Effect.succeed({ message: "" })), ) if (errorBody.message.toLowerCase().includes("rate limit")) { return yield* Effect.fail( @@ -482,7 +481,7 @@ export class GitHubApiClient extends Effect.Service()("GitHubAp if (response.status >= 400) { const errorBody = yield* response.json.pipe( Effect.flatMap(Schema.decodeUnknown(GitHubErrorApiResponse)), - Effect.catchAll((error) => + Effect.catch((error) => Effect.logDebug(`Failed to parse GitHub error response: ${String(error)}`).pipe( Effect.as({ message: "Unknown error" }), ), @@ -560,7 +559,7 @@ export class GitHubApiClient extends Effect.Service()("GitHubAp if (response.status >= 400) { const errorBody = yield* response.json.pipe( Effect.flatMap(Schema.decodeUnknown(GitHubErrorApiResponse)), - Effect.catchAll((error) => + Effect.catch((error) => Effect.logDebug(`Failed to parse GitHub error response: ${String(error)}`).pipe( Effect.as({ message: "Unknown error" }), ), @@ -640,7 +639,7 @@ export class GitHubApiClient extends Effect.Service()("GitHubAp if (reposResponse.status >= 200 && reposResponse.status < 300) { const data = yield* reposResponse.json.pipe( Effect.flatMap(Schema.decodeUnknown(GitHubRepositoriesApiResponse)), - Effect.catchAll((error) => + Effect.catch((error) => Effect.logDebug( `Failed to parse GitHub repositories response: ${String(error)}`, ).pipe(Effect.as({ total_count: 0, repositories: [] })), @@ -676,7 +675,7 @@ export class GitHubApiClient extends Effect.Service()("GitHubAp if (appResponse.status >= 200 && appResponse.status < 300) { const appData = yield* appResponse.json.pipe( Effect.flatMap(Schema.decodeUnknown(GitHubAppApiResponse)), - Effect.catchAll((error) => + Effect.catch((error) => Effect.logDebug(`Failed to parse GitHub App response: ${String(error)}`).pipe( Effect.as({ id: 0, name: "GitHub App" }), ), diff --git a/packages/integrations/src/github/jwt-service.ts b/packages/integrations/src/github/jwt-service.ts index 4508521fe..c5ae9b13d 100644 --- a/packages/integrations/src/github/jwt-service.ts +++ b/packages/integrations/src/github/jwt-service.ts @@ -1,6 +1,6 @@ import { createPrivateKey } from "node:crypto" -import { FetchHttpClient, HttpClient, HttpClientRequest } from "@effect/platform" -import { Config, Effect, Redacted, Schema } from "effect" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { ServiceMap, Config, Effect, Redacted, Schema } from "effect" import { SignJWT } from "jose" // ============================================================================ @@ -10,7 +10,7 @@ import { SignJWT } from "jose" /** * Error when JWT generation fails. */ -export class GitHubAppJWTError extends Schema.TaggedError()("GitHubAppJWTError", { +export class GitHubAppJWTError extends Schema.TaggedErrorClass()("GitHubAppJWTError", { message: Schema.String, cause: Schema.optional(Schema.Unknown), }) {} @@ -18,7 +18,7 @@ export class GitHubAppJWTError extends Schema.TaggedError()(" /** * Error when installation token generation fails. */ -export class GitHubInstallationTokenError extends Schema.TaggedError()( +export class GitHubInstallationTokenError extends Schema.TaggedErrorClass()( "GitHubInstallationTokenError", { installationId: Schema.String, @@ -157,9 +157,8 @@ const GITHUB_API_BASE_URL = "https://api.github.com" * // token.expiresAt is when it expires (1 hour from now) * ``` */ -export class GitHubAppJWTService extends Effect.Service()("GitHubAppJWTService", { - accessors: true, - effect: Effect.gen(function* () { +export class GitHubAppJWTService extends ServiceMap.Service()("GitHubAppJWTService", { + make: Effect.gen(function* () { // Load config once at service initialization // Use orDie since missing config is a fatal startup error const config = yield* loadGitHubAppConfig.pipe(Effect.orDie) @@ -210,7 +209,7 @@ export class GitHubAppJWTService extends Effect.Service()(" if (response.status >= 400) { const errorBody = yield* response.json.pipe( Effect.flatMap(Schema.decodeUnknown(GitHubErrorApiResponse)), - Effect.catchAll((error) => + Effect.catch((error) => Effect.logDebug(`Failed to parse GitHub error response: ${String(error)}`).pipe( Effect.as({ message: "Unknown error" }), ), diff --git a/packages/integrations/src/linear/api-client.ts b/packages/integrations/src/linear/api-client.ts index 077941dd2..05426100a 100644 --- a/packages/integrations/src/linear/api-client.ts +++ b/packages/integrations/src/linear/api-client.ts @@ -5,8 +5,8 @@ * retries, and proper error handling. */ -import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "@effect/platform" -import { Duration, Effect, Layer, Option, Schedule, Schema } from "effect" +import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { ServiceMap, Duration, Effect, Layer, Option, Schedule, Schema } from "effect" // ============================================================================ // Configuration @@ -87,25 +87,25 @@ export type LinearAccountInfo = typeof LinearAccountInfo.Type // Error Types // ============================================================================ -export class LinearApiError extends Schema.TaggedError()("LinearApiError", { +export class LinearApiError extends Schema.TaggedErrorClass()("LinearApiError", { message: Schema.String, status: Schema.optional(Schema.Number), cause: Schema.optional(Schema.Unknown), }) {} -export class LinearRateLimitError extends Schema.TaggedError()("LinearRateLimitError", { +export class LinearRateLimitError extends Schema.TaggedErrorClass()("LinearRateLimitError", { message: Schema.String, retryAfter: Schema.optional(Schema.Number), }) {} -export class LinearIssueNotFoundError extends Schema.TaggedError()( +export class LinearIssueNotFoundError extends Schema.TaggedErrorClass()( "LinearIssueNotFoundError", { issueId: Schema.String, }, ) {} -export class LinearTeamNotFoundError extends Schema.TaggedError()( +export class LinearTeamNotFoundError extends Schema.TaggedErrorClass()( "LinearTeamNotFoundError", { message: Schema.String, @@ -418,9 +418,8 @@ const isRetryableError = ( * const issue = yield* client.fetchIssue("ENG-123", accessToken) * ``` */ -export class LinearApiClient extends Effect.Service()("LinearApiClient", { - accessors: true, - effect: Effect.gen(function* () { +export class LinearApiClient extends ServiceMap.Service()("LinearApiClient", { + make: Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient /** diff --git a/packages/rivet-effect/package.json b/packages/rivet-effect/package.json index 02cfcdab4..6a97682c5 100644 --- a/packages/rivet-effect/package.json +++ b/packages/rivet-effect/package.json @@ -15,8 +15,7 @@ "rivetkit": "2.1.6" }, "devDependencies": { - "@effect/language-service": "catalog:effect", - "@effect/vitest": "^0.27.0", + "@effect/vitest": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3" } diff --git a/packages/rivet-effect/src/actor.ts b/packages/rivet-effect/src/actor.ts index 1f1feb3e5..ceda6a071 100644 --- a/packages/rivet-effect/src/actor.ts +++ b/packages/rivet-effect/src/actor.ts @@ -1,4 +1,4 @@ -import { Cause, Context, Effect, Exit } from "effect" +import { Cause, Effect, Exit, ServiceMap } from "effect" import type { ActorContext } from "rivetkit" import type { YieldWrap } from "effect/Utils" import { StatePersistenceError } from "./errors.ts" @@ -7,19 +7,18 @@ import { runPromise, runPromiseExit } from "./runtime.ts" type AnyActorContext = ActorContext /** - * Context.Tag for injecting Rivet's ActorContext into Effect pipelines. + * ServiceMap.Service for injecting Rivet's ActorContext into Effect pipelines. * - * Uses Context.Tag (not Effect.Service) because the actor context is an + * Uses ServiceMap.Service (not Effect.Service) because the actor context is an * externally-provided runtime resource injected by the Rivet framework, * not a service we construct via layers. */ -export class RivetActorContext extends Context.Tag("@hazel/rivet-effect/RivetActorContext")< - RivetActorContext, +export class RivetActorContext extends ServiceMap.Service() {} +>()("@hazel/rivet-effect/RivetActorContext") {} export const provideActorContext = ( - effect: Effect.Effect, + make: Effect.Effect, context: unknown, ): Effect.Effect> => Effect.provideService( @@ -152,7 +151,7 @@ const runEffectOnActorContext = (c: unknown, effect: Effect.Effect( c: ActorContext, - effect: Effect.Effect, + make: Effect.Effect, ): Effect.Effect => Effect.sync(() => { const promise = runPromiseExit(effect, c).then((exit) => { diff --git a/packages/rivet-effect/src/errors.ts b/packages/rivet-effect/src/errors.ts index b7b150868..442b62ead 100644 --- a/packages/rivet-effect/src/errors.ts +++ b/packages/rivet-effect/src/errors.ts @@ -1,6 +1,6 @@ import { Cause, Schema } from "effect" -export class RuntimeExecutionError extends Schema.TaggedError()( +export class RuntimeExecutionError extends Schema.TaggedErrorClass()( "RuntimeExecutionError", { message: Schema.String, @@ -16,7 +16,7 @@ export const makeRuntimeExecutionError = (operation: string, cause: Cause.Cause< cause: Cause.pretty(cause), }) -export class StatePersistenceError extends Schema.TaggedError()( +export class StatePersistenceError extends Schema.TaggedErrorClass()( "StatePersistenceError", { message: Schema.String, diff --git a/packages/rivet-effect/src/lifecycle.ts b/packages/rivet-effect/src/lifecycle.ts index 4f14d6ff7..94d3d72c4 100644 --- a/packages/rivet-effect/src/lifecycle.ts +++ b/packages/rivet-effect/src/lifecycle.ts @@ -25,7 +25,7 @@ const runWithContext = (context: unknown, effect: Effect.Effect( context: unknown, - effect: Effect.Effect, + make: Effect.Effect, ): Promise> => runPromiseExit(provideActorContext(effect, context), context) const runGeneratorWithContext = ( diff --git a/packages/rivet-effect/src/runtime.ts b/packages/rivet-effect/src/runtime.ts index 83c33b647..202af9764 100644 --- a/packages/rivet-effect/src/runtime.ts +++ b/packages/rivet-effect/src/runtime.ts @@ -99,7 +99,7 @@ export const runPromise = (effect: Effect.Effect, context?: un }) export const runPromiseExit = ( - effect: Effect.Effect, + make: Effect.Effect, context?: unknown, ): Promise> => { const runtime = getManagedRuntime(context) diff --git a/packages/schema/package.json b/packages/schema/package.json index 9fffeb834..3b99943ae 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -8,11 +8,9 @@ ".": "./src/index.ts" }, "dependencies": { - "@effect/platform": "catalog:effect", "effect": "catalog:effect" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3" } diff --git a/packages/schema/src/avatar-url.ts b/packages/schema/src/avatar-url.ts index 71fcc479e..1254992c4 100644 --- a/packages/schema/src/avatar-url.ts +++ b/packages/schema/src/avatar-url.ts @@ -1,7 +1,7 @@ -import { HttpClient } from "@effect/platform" +import { HttpClient } from "effect/unstable/http" import { Duration, Effect, Option, Schema } from "effect" -export class InvalidAvatarUrlError extends Schema.TaggedError()( +export class InvalidAvatarUrlError extends Schema.TaggedErrorClass()( "InvalidAvatarUrlError", { message: Schema.String, @@ -74,7 +74,7 @@ export const AvatarUrl = Schema.String.pipe( Schema.filterEffect((url) => validateImageUrl(url).pipe( Effect.map(() => true), - Effect.catchAll((e) => Effect.succeed(e.message)), + Effect.catch((e) => Effect.succeed(e.message)), ), ), ).annotations({ diff --git a/packages/setup/package.json b/packages/setup/package.json index 0fd5aa9db..2839f13ee 100644 --- a/packages/setup/package.json +++ b/packages/setup/package.json @@ -11,11 +11,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@effect/cli": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-bun": "catalog:effect", - "@effect/printer": "catalog:effect", - "@effect/printer-ansi": "catalog:effect", "@hazel/db": "workspace:*", "@hazel/schema": "workspace:*", "@workos-inc/node": "^7.77.0", @@ -23,7 +19,6 @@ "picocolors": "^1.1.1" }, "devDependencies": { - "@effect/language-service": "catalog:effect", "@types/bun": "1.3.9", "typescript": "^5.9.3" } diff --git a/packages/setup/src/commands/bots.ts b/packages/setup/src/commands/bots.ts index 7f8cca87c..4a015c3b2 100644 --- a/packages/setup/src/commands/bots.ts +++ b/packages/setup/src/commands/bots.ts @@ -1,4 +1,4 @@ -import { Command, Options, Prompt } from "@effect/cli" +import { Command, Options, Prompt } from "effect/unstable/cli" import { Database, schema, isNull } from "@hazel/db" import type { BotId, BotInstallationId, OrganizationId, OrganizationMemberId, UserId } from "@hazel/schema" import { Console, Effect, Option, Redacted } from "effect" diff --git a/packages/setup/src/commands/certs.ts b/packages/setup/src/commands/certs.ts index 7f9a47e79..1a9ce4441 100644 --- a/packages/setup/src/commands/certs.ts +++ b/packages/setup/src/commands/certs.ts @@ -1,4 +1,4 @@ -import { Command, Prompt } from "@effect/cli" +import { Command, Prompt } from "effect/unstable/cli" import { Console, Effect } from "effect" import pc from "picocolors" import { CertManager } from "../services/cert-manager.ts" diff --git a/packages/setup/src/commands/doctor.ts b/packages/setup/src/commands/doctor.ts index bf147e67d..a9f52e876 100644 --- a/packages/setup/src/commands/doctor.ts +++ b/packages/setup/src/commands/doctor.ts @@ -1,4 +1,4 @@ -import { Command } from "@effect/cli" +import { Command } from "effect/unstable/cli" import { Console, Effect } from "effect" import pc from "picocolors" import { Doctor, type CheckResult } from "../services/doctor.ts" diff --git a/packages/setup/src/commands/env.ts b/packages/setup/src/commands/env.ts index 3774c4d8e..9f287c20a 100644 --- a/packages/setup/src/commands/env.ts +++ b/packages/setup/src/commands/env.ts @@ -1,4 +1,4 @@ -import { Command, Options, Prompt } from "@effect/cli" +import { Command, Options, Prompt } from "effect/unstable/cli" import { Console, Effect, Redacted } from "effect" import pc from "picocolors" import { SecretGenerator } from "../services/secrets.ts" diff --git a/packages/setup/src/commands/setup.ts b/packages/setup/src/commands/setup.ts index f7b767fdc..7a62b4582 100644 --- a/packages/setup/src/commands/setup.ts +++ b/packages/setup/src/commands/setup.ts @@ -1,4 +1,4 @@ -import { Command, Options, Prompt } from "@effect/cli" +import { Command, Options, Prompt } from "effect/unstable/cli" import { Console, Effect, Redacted } from "effect" import pc from "picocolors" import { SecretGenerator } from "../services/secrets.ts" @@ -70,7 +70,7 @@ export const setupCommand = Command.make( error: undefined, } }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.succeed({ ok: false, exitCode: null, @@ -569,7 +569,7 @@ export const setupCommand = Command.make( return proc.exitCode === 0 }, catch: () => false, - }).pipe(Effect.catchAll(() => Effect.succeed(false))) + }).pipe(Effect.catch(() => Effect.succeed(false))) if (dbPushResult) { yield* Console.log(pc.green("\n\u2713") + " Database schema pushed") diff --git a/packages/setup/src/index.ts b/packages/setup/src/index.ts index 08a24a562..c3be516f4 100644 --- a/packages/setup/src/index.ts +++ b/packages/setup/src/index.ts @@ -1,5 +1,5 @@ #!/usr/bin/env bun -import { Command } from "@effect/cli" +import { Command } from "effect/unstable/cli" import { BunContext, BunRuntime } from "@effect/platform-bun" import { Effect, Layer } from "effect" import { existsSync, readFileSync } from "fs" diff --git a/packages/setup/src/prompts.ts b/packages/setup/src/prompts.ts index 70269acb8..530496e21 100644 --- a/packages/setup/src/prompts.ts +++ b/packages/setup/src/prompts.ts @@ -1,4 +1,4 @@ -import { Prompt } from "@effect/cli" +import { Prompt } from "effect/unstable/cli" import { Effect, Redacted } from "effect" import type { EnvReadResult } from "./services/env-writer.ts" import { getEnvValues, maskSecret, type EnvValue } from "./templates.ts" diff --git a/packages/setup/src/services/cert-manager.ts b/packages/setup/src/services/cert-manager.ts index b6965be3e..d5a50a662 100644 --- a/packages/setup/src/services/cert-manager.ts +++ b/packages/setup/src/services/cert-manager.ts @@ -1,4 +1,4 @@ -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" import { resolve } from "node:path" export interface CertPaths { @@ -16,9 +16,8 @@ const findRepoRoot = (): string => { return process.cwd() } -export class CertManager extends Effect.Service()("CertManager", { - accessors: true, - effect: Effect.succeed({ +export class CertManager extends ServiceMap.Service()("CertManager", { + make: Effect.succeed({ get certsDir() { return resolve(findRepoRoot(), "certs") }, @@ -51,7 +50,7 @@ export class CertManager extends Effect.Service()("CertManager", { return (await proc.exited) === 0 }, catch: () => new Error("mkcert check failed"), - }).pipe(Effect.catchAll(() => Effect.succeed(false))), + }).pipe(Effect.catch(() => Effect.succeed(false))), installMkcert: () => Effect.tryPromise({ diff --git a/packages/setup/src/services/doctor.ts b/packages/setup/src/services/doctor.ts index c126d8eaa..b549fd0b0 100644 --- a/packages/setup/src/services/doctor.ts +++ b/packages/setup/src/services/doctor.ts @@ -1,4 +1,4 @@ -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" export interface CheckResult { name: string @@ -29,14 +29,13 @@ const checkContainer = (containerName: string, displayName: string): Effect.Effe }, catch: () => new Error("Check failed"), }).pipe( - Effect.catchAll(() => + Effect.catch(() => Effect.succeed({ name: displayName, status: "fail" as const, message: "Not running" }), ), ) -export class Doctor extends Effect.Service()("Doctor", { - accessors: true, - effect: Effect.succeed({ +export class Doctor extends ServiceMap.Service()("Doctor", { + make: Effect.succeed({ checkBun: (): Effect.Effect => Effect.tryPromise({ try: async () => { @@ -47,7 +46,7 @@ export class Doctor extends Effect.Service()("Doctor", { }, catch: () => new Error("Bun not found"), }).pipe( - Effect.catchAll(() => + Effect.catch(() => Effect.succeed({ name: "Bun", status: "fail" as const, @@ -66,7 +65,7 @@ export class Doctor extends Effect.Service()("Doctor", { }, catch: () => new Error("Docker not running"), }).pipe( - Effect.catchAll(() => + Effect.catch(() => Effect.succeed({ name: "Docker", status: "fail" as const, @@ -97,7 +96,7 @@ export class Doctor extends Effect.Service()("Doctor", { }, catch: () => new Error("Could not check containers"), }).pipe( - Effect.catchAll(() => + Effect.catch(() => Effect.succeed({ name: "Docker Compose", status: "warn" as const, diff --git a/packages/setup/src/services/env-writer.ts b/packages/setup/src/services/env-writer.ts index 487ac1560..f3c90a652 100644 --- a/packages/setup/src/services/env-writer.ts +++ b/packages/setup/src/services/env-writer.ts @@ -1,4 +1,4 @@ -import { Console, Effect } from "effect" +import { ServiceMap, Console, Effect } from "effect" import { dirname } from "node:path" import { mkdir } from "node:fs/promises" @@ -46,9 +46,8 @@ const parseEnvContent = (content: string): Record => { return result } -export class EnvWriter extends Effect.Service()("EnvWriter", { - accessors: true, - effect: Effect.succeed({ +export class EnvWriter extends ServiceMap.Service()("EnvWriter", { + make: Effect.succeed({ writeEnvFile: (filePath: string, vars: Record, dryRun: boolean = false) => Effect.gen(function* () { const content = Object.entries(vars) diff --git a/packages/setup/src/services/secrets.ts b/packages/setup/src/services/secrets.ts index 18f7e58a0..3d172e789 100644 --- a/packages/setup/src/services/secrets.ts +++ b/packages/setup/src/services/secrets.ts @@ -1,8 +1,7 @@ -import { Effect } from "effect" +import { ServiceMap, Effect } from "effect" -export class SecretGenerator extends Effect.Service()("SecretGenerator", { - accessors: true, - effect: Effect.succeed({ +export class SecretGenerator extends ServiceMap.Service()("SecretGenerator", { + make: Effect.succeed({ generatePassword: (length: number): string => { const bytes = new Uint8Array(length) crypto.getRandomValues(bytes) diff --git a/packages/setup/src/services/validators.ts b/packages/setup/src/services/validators.ts index 410b78ca3..f36789469 100644 --- a/packages/setup/src/services/validators.ts +++ b/packages/setup/src/services/validators.ts @@ -1,4 +1,4 @@ -import { Data, Effect } from "effect" +import { ServiceMap, Data, Effect } from "effect" import { WorkOS } from "@workos-inc/node" import { SQL } from "bun" @@ -7,9 +7,8 @@ export class ValidationError extends Data.TaggedError("ValidationError")<{ message: string }> {} -export class CredentialValidator extends Effect.Service()("CredentialValidator", { - accessors: true, - effect: Effect.succeed({ +export class CredentialValidator extends ServiceMap.Service()("CredentialValidator", { + make: Effect.succeed({ validateWorkOS: (apiKey: string, _clientId: string) => Effect.tryPromise({ try: async () => { From b4e7342938bd5b5cddff1dba073bf7190c3472f1 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 00:11:04 +0100 Subject: [PATCH 02/34] stuff --- apps/backend/src/lib/env-vars.ts | 5 +- .../backend/src/policies/attachment-policy.ts | 21 +++--- apps/backend/src/policies/bot-policy.ts | 10 ++- .../src/policies/channel-member-policy.ts | 17 ++--- apps/backend/src/policies/channel-policy.ts | 10 ++- .../src/policies/channel-section-policy.ts | 10 ++- .../src/policies/channel-webhook-policy.ts | 11 ++- .../src/policies/custom-emoji-policy.ts | 10 ++- .../policies/github-subscription-policy.ts | 11 ++- .../policies/integration-connection-policy.ts | 9 ++- .../backend/src/policies/invitation-policy.ts | 17 ++--- apps/backend/src/policies/message-policy.ts | 17 ++--- .../src/policies/message-reaction-policy.ts | 17 ++--- .../src/policies/notification-policy.ts | 10 ++- .../policies/organization-member-policy.ts | 10 ++- .../src/policies/organization-policy.ts | 9 ++- .../src/policies/pinned-message-policy.ts | 17 ++--- .../src/policies/rss-subscription-policy.ts | 11 ++- .../src/policies/typing-indicator-policy.ts | 10 ++- apps/backend/src/policies/user-policy.ts | 7 +- .../policies/user-presence-status-policy.ts | 7 +- apps/backend/src/routes/integrations.http.ts | 8 +-- apps/backend/src/rpc/middleware/auth-class.ts | 11 +-- .../src/services/bot-gateway-service.ts | 10 ++- .../chat-sync-attribution-reconciler.ts | 11 ++- .../chat-sync/chat-sync-core-worker.ts | 39 +++++----- .../chat-sync/chat-sync-provider-registry.ts | 9 ++- .../chat-sync/discord-gateway-service.ts | 8 ++- .../services/chat-sync/discord-sync-worker.ts | 9 ++- .../services/connect-conversation-service.ts | 25 +++---- .../src/services/integration-encryption.ts | 6 +- .../src/services/integration-token-service.ts | 19 ++--- .../integrations/integration-bot-service.ts | 17 ++--- .../src/services/message-outbox-dispatcher.ts | 11 ++- .../services/message-side-effect-service.ts | 9 ++- .../src/services/mock-data-generator.ts | 9 ++- .../src/services/oauth/oauth-http-client.ts | 9 ++- .../services/oauth/oauth-provider-registry.ts | 10 ++- .../src/services/oauth/oauth-provider.ts | 2 +- apps/backend/src/services/org-resolver.ts | 17 ++--- apps/backend/src/services/rate-limiter.ts | 7 +- apps/backend/src/services/session-manager.ts | 10 ++- .../src/services/webhook-bot-service.ts | 10 ++- apps/backend/src/services/workos-auth.ts | 4 +- apps/backend/src/services/workos-webhook.ts | 4 +- apps/bot-gateway/src/index.ts | 15 ++-- apps/cluster/src/services/bot-user-service.ts | 4 +- .../src/workflows/github-webhook-handler.ts | 2 +- .../workflows/message-notification-handler.ts | 2 +- .../src/workflows/rss-feed-poll-handler.ts | 2 +- .../src/cache/access-context-cache.ts | 2 +- apps/electric-proxy/src/config.ts | 4 +- apps/link-preview-worker/src/api.ts | 2 +- apps/link-preview-worker/src/declare.ts | 6 +- .../src/services/twitter.ts | 9 ++- .../web/src/atoms/notification-sound-atoms.ts | 2 +- apps/web/src/atoms/search-atoms.ts | 2 +- apps/web/src/lib/error-messages.ts | 4 +- .../web/src/lib/services/common/api-client.ts | 7 +- .../src/lib/services/common/network-mode.ts | 5 +- .../src/lib/services/desktop/tauri-auth.ts | 10 ++- .../lib/services/desktop/token-exchange.ts | 7 +- bots/hazel-bot/src/tools/base.ts | 6 +- bots/hazel-bot/src/tools/craft.ts | 30 ++++---- bots/hazel-bot/src/tools/linear.ts | 42 +++++------ libs/ai-openrouter/src/OpenRouterClient.ts | 7 +- libs/ai-openrouter/src/OpenRouterConfig.ts | 7 +- .../src/OpenRouterLanguageModel.ts | 7 +- libs/bot-sdk/src/auth.ts | 4 +- libs/bot-sdk/src/hazel-bot-sdk.ts | 11 +-- libs/bot-sdk/src/services/health-server.ts | 4 +- libs/bot-sdk/src/streaming/actors-client.ts | 4 +- libs/bot-sdk/src/streaming/types.ts | 2 +- .../src/errors.ts | 2 +- .../src/tanstack-errors.ts | 20 +++--- packages/actors/src/auth/jwks-service.ts | 9 ++- .../src/auth/token-validation-service.ts | 10 ++- packages/auth/src/consumers/backend-auth.ts | 5 +- packages/auth/src/consumers/proxy-auth.ts | 10 ++- packages/auth/src/session/workos-client.ts | 5 +- .../src/repositories/attachment-repo.ts | 4 +- .../src/repositories/bot-command-repo.ts | 4 +- .../src/repositories/bot-installation-repo.ts | 4 +- .../backend-core/src/repositories/bot-repo.ts | 4 +- .../src/repositories/channel-member-repo.ts | 4 +- .../src/repositories/channel-repo.ts | 4 +- .../src/repositories/channel-section-repo.ts | 4 +- .../src/repositories/channel-webhook-repo.ts | 4 +- .../src/repositories/connect-invite-repo.ts | 4 +- .../src/repositories/custom-emoji-repo.ts | 4 +- .../repositories/integration-token-repo.ts | 4 +- .../src/repositories/invitation-repo.ts | 4 +- .../src/repositories/message-outbox-repo.ts | 14 ++-- .../src/repositories/message-reaction-repo.ts | 4 +- .../src/repositories/message-repo.ts | 4 +- .../src/repositories/notification-repo.ts | 4 +- .../src/repositories/organization-repo.ts | 10 ++- .../src/repositories/pinned-message-repo.ts | 4 +- .../src/repositories/rss-subscription-repo.ts | 4 +- .../src/repositories/typing-indicator-repo.ts | 4 +- .../src/repositories/user-repo.ts | 4 +- .../backend-core/src/services/workos-sync.ts | 19 ++--- packages/backend-core/src/services/workos.ts | 4 +- packages/db/src/services/drizzle-effect.ts | 10 +-- packages/domain/src/bot-gateway.ts | 12 ++-- .../cluster/activities/cleanup-activities.ts | 2 +- .../cluster/activities/github-activities.ts | 4 +- .../github-installation-activities.ts | 4 +- .../cluster/activities/message-activities.ts | 2 +- .../src/cluster/activities/rss-activities.ts | 4 +- .../activities/thread-naming-activities.ts | 6 +- .../workflows/github-installation-workflow.ts | 4 +- packages/domain/src/current-user.ts | 15 ++-- packages/domain/src/desktop-auth-errors.ts | 6 +- packages/domain/src/errors.ts | 17 +++-- packages/domain/src/http/api-v1/messages.ts | 14 ++-- packages/domain/src/http/api.ts | 2 +- packages/domain/src/http/auth.ts | 12 ++-- packages/domain/src/http/bot-commands.ts | 16 ++--- packages/domain/src/http/chat-sync.ts | 12 ++-- packages/domain/src/http/incoming-webhooks.ts | 14 ++-- .../domain/src/http/integration-commands.ts | 4 +- .../domain/src/http/integration-resources.ts | 12 ++-- packages/domain/src/http/integrations.ts | 10 +-- packages/domain/src/http/internal.ts | 2 +- packages/domain/src/http/klipy.ts | 2 +- packages/domain/src/http/mock-data.ts | 2 +- packages/domain/src/http/presence.ts | 2 +- packages/domain/src/http/uploads.ts | 14 ++-- packages/domain/src/http/webhooks.ts | 8 +-- .../domain/src/models/attachment-model.ts | 2 +- .../domain/src/models/bot-command-model.ts | 2 +- packages/domain/src/models/channel-model.ts | 2 +- .../models/chat-sync-channel-link-model.ts | 8 +-- .../src/models/chat-sync-connection-model.ts | 2 +- .../models/chat-sync-event-receipt-model.ts | 4 +- .../connect-conversation-channel-model.ts | 2 +- .../src/models/connect-conversation-model.ts | 2 +- .../domain/src/models/connect-invite-model.ts | 4 +- .../models/integration-connection-model.ts | 6 +- .../src/models/integration-request-model.ts | 2 +- .../domain/src/models/invitation-model.ts | 2 +- .../domain/src/models/message-embed-schema.ts | 14 ++-- .../models/message-integration-link-model.ts | 2 +- .../src/models/organization-member-model.ts | 2 +- packages/domain/src/models/theme-model.ts | 10 +-- .../src/models/typing-indicator-model.ts | 2 +- packages/domain/src/models/user-model.ts | 2 +- .../src/models/user-presence-status-model.ts | 2 +- packages/domain/src/models/utils.ts | 6 +- packages/domain/src/rate-limit-errors.ts | 3 +- packages/domain/src/rpc/attachments.ts | 6 +- packages/domain/src/rpc/bots.ts | 30 ++++---- packages/domain/src/rpc/channel-members.ts | 8 +-- packages/domain/src/rpc/channel-sections.ts | 12 ++-- packages/domain/src/rpc/channel-webhooks.ts | 14 ++-- packages/domain/src/rpc/channels.ts | 16 ++--- packages/domain/src/rpc/chat-sync.ts | 18 ++--- packages/domain/src/rpc/connect-shares.ts | 30 ++++---- packages/domain/src/rpc/custom-emojis.ts | 14 ++-- .../domain/src/rpc/github-subscriptions.ts | 12 ++-- .../domain/src/rpc/integration-requests.ts | 2 +- packages/domain/src/rpc/invitations.ts | 12 ++-- packages/domain/src/rpc/message-reactions.ts | 8 +-- packages/domain/src/rpc/messages.ts | 12 ++-- packages/domain/src/rpc/middleware.ts | 11 +-- packages/domain/src/rpc/notifications.ts | 8 +-- .../domain/src/rpc/organization-members.ts | 8 +-- packages/domain/src/rpc/organizations.ts | 30 ++++---- packages/domain/src/rpc/pinned-messages.ts | 6 +- packages/domain/src/rpc/rss-subscriptions.ts | 12 ++-- .../src/rpc/scope-injection-middleware.ts | 2 +- packages/domain/src/rpc/typing-indicators.ts | 6 +- .../domain/src/rpc/user-presence-status.ts | 6 +- packages/domain/src/rpc/users.ts | 10 +-- packages/domain/src/scopes/api-scope.ts | 4 +- .../domain/src/scopes/permission-error.ts | 3 +- packages/domain/src/session-errors.ts | 17 +++-- packages/effect-bun/src/Redis.ts | 9 ++- packages/effect-bun/src/S3.ts | 7 +- packages/integrations/src/craft/api-client.ts | 13 ++-- .../integrations/src/discord/api-client.ts | 9 ++- .../integrations/src/github/api-client.ts | 11 +-- .../integrations/src/github/jwt-service.ts | 9 ++- .../integrations/src/linear/api-client.ts | 7 +- packages/schema/src/avatar-url.ts | 2 +- packages/schema/src/ids.ts | 72 +++++++++---------- packages/schema/src/workos.ts | 12 ++-- packages/setup/src/services/cert-manager.ts | 4 +- packages/setup/src/services/doctor.ts | 4 +- packages/setup/src/services/env-writer.ts | 4 +- packages/setup/src/services/secrets.ts | 4 +- packages/setup/src/services/validators.ts | 4 +- 193 files changed, 965 insertions(+), 729 deletions(-) diff --git a/apps/backend/src/lib/env-vars.ts b/apps/backend/src/lib/env-vars.ts index 82527a426..a84e0691d 100644 --- a/apps/backend/src/lib/env-vars.ts +++ b/apps/backend/src/lib/env-vars.ts @@ -1,5 +1,6 @@ import * as Config from "effect/Config" import * as Effect from "effect/Effect" +import * as ServiceMap from "effect/ServiceMap" export class EnvVars extends ServiceMap.Service()("EnvVars", { make: Effect.gen(function* () { @@ -8,4 +9,6 @@ export class EnvVars extends ServiceMap.Service()("EnvVars", { DATABASE_URL: yield* Config.redacted("DATABASE_URL"), } as const }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/apps/backend/src/policies/attachment-policy.ts b/apps/backend/src/policies/attachment-policy.ts index eff4cd292..f5823056a 100644 --- a/apps/backend/src/policies/attachment-policy.ts +++ b/apps/backend/src/policies/attachment-policy.ts @@ -7,7 +7,7 @@ import { } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { AttachmentId } from "@hazel/schema" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" import { isAdminOrOwner } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" @@ -160,12 +160,13 @@ export class AttachmentPolicy extends ServiceMap.Service()("At return { canCreate, canUpdate, canDelete, canView } as const }), - dependencies: [ - AttachmentRepo.Default, - MessageRepo.Default, - ChannelRepo.Default, - OrganizationMemberRepo.Default, - ChannelMemberRepo.Default, - OrgResolver.Default, - ], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(AttachmentRepo.Default), + Layer.provide(MessageRepo.Default), + Layer.provide(ChannelRepo.Default), + Layer.provide(OrganizationMemberRepo.Default), + Layer.provide(ChannelMemberRepo.Default), + Layer.provide(OrgResolver.Default), + ) +} diff --git a/apps/backend/src/policies/bot-policy.ts b/apps/backend/src/policies/bot-policy.ts index d923bd6cd..b09628665 100644 --- a/apps/backend/src/policies/bot-policy.ts +++ b/apps/backend/src/policies/bot-policy.ts @@ -1,7 +1,7 @@ import { BotRepo } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { BotId, OrganizationId } from "@hazel/schema" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" @@ -109,5 +109,9 @@ export class BotPolicy extends ServiceMap.Service()("BotPolicy/Policy return { canCreate, canRead, canUpdate, canDelete, canInstall, canUninstall } as const }), - dependencies: [BotRepo.Default, OrgResolver.Default], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(BotRepo.Default), + Layer.provide(OrgResolver.Default), + ) +} diff --git a/apps/backend/src/policies/channel-member-policy.ts b/apps/backend/src/policies/channel-member-policy.ts index 5c921fa64..6d0864746 100644 --- a/apps/backend/src/policies/channel-member-policy.ts +++ b/apps/backend/src/policies/channel-member-policy.ts @@ -1,7 +1,7 @@ import { ChannelMemberRepo, ChannelRepo, OrganizationMemberRepo } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { ChannelId, ChannelMemberId } from "@hazel/schema" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" import { isAdminOrOwner } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" @@ -164,10 +164,11 @@ export class ChannelMemberPolicy extends ServiceMap.Service return { canCreate, canRead, canUpdate, canDelete, isOwner } as const }), - dependencies: [ - ChannelMemberRepo.Default, - ChannelRepo.Default, - OrganizationMemberRepo.Default, - OrgResolver.Default, - ], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(ChannelMemberRepo.Default), + Layer.provide(ChannelRepo.Default), + Layer.provide(OrganizationMemberRepo.Default), + Layer.provide(OrgResolver.Default), + ) +} diff --git a/apps/backend/src/policies/channel-policy.ts b/apps/backend/src/policies/channel-policy.ts index 5024ac1e6..c9869642a 100644 --- a/apps/backend/src/policies/channel-policy.ts +++ b/apps/backend/src/policies/channel-policy.ts @@ -1,7 +1,7 @@ import { ChannelRepo } from "@hazel/backend-core" import { ErrorUtils } from "@hazel/domain" import type { ChannelId, OrganizationId } from "@hazel/schema" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" @@ -58,5 +58,9 @@ export class ChannelPolicy extends ServiceMap.Service()("ChannelP return { canUpdate, canDelete, canCreate } as const }), - dependencies: [ChannelRepo.Default, OrgResolver.Default], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(ChannelRepo.Default), + Layer.provide(OrgResolver.Default), + ) +} diff --git a/apps/backend/src/policies/channel-section-policy.ts b/apps/backend/src/policies/channel-section-policy.ts index fec9b0b9b..43a646399 100644 --- a/apps/backend/src/policies/channel-section-policy.ts +++ b/apps/backend/src/policies/channel-section-policy.ts @@ -1,7 +1,7 @@ import { ChannelSectionRepo } from "@hazel/backend-core" import { ErrorUtils } from "@hazel/domain" import type { ChannelSectionId, OrganizationId } from "@hazel/schema" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" @@ -70,6 +70,10 @@ export class ChannelSectionPolicy extends ServiceMap.Service()(" return { canCreate, canUpdate, canDelete } as const }), - dependencies: [CustomEmojiRepo.Default, OrgResolver.Default], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(CustomEmojiRepo.Default), + Layer.provide(OrgResolver.Default), + ) +} diff --git a/apps/backend/src/policies/github-subscription-policy.ts b/apps/backend/src/policies/github-subscription-policy.ts index 3ebaf920b..95c8af718 100644 --- a/apps/backend/src/policies/github-subscription-policy.ts +++ b/apps/backend/src/policies/github-subscription-policy.ts @@ -1,7 +1,7 @@ import { ChannelRepo, GitHubSubscriptionRepo } from "@hazel/backend-core" import { ErrorUtils } from "@hazel/domain" import type { ChannelId, GitHubSubscriptionId, OrganizationId } from "@hazel/schema" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" @@ -96,6 +96,11 @@ export class GitHubSubscriptionPolicy extends ServiceMap.Service()("In return { canRead, canCreate, canUpdate, canDelete, canAccept, canList } as const }), - dependencies: [ - InvitationRepo.Default, - OrganizationMemberRepo.Default, - UserRepo.Default, - OrgResolver.Default, - ], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(InvitationRepo.Default), + Layer.provide(OrganizationMemberRepo.Default), + Layer.provide(UserRepo.Default), + Layer.provide(OrgResolver.Default), + ) +} diff --git a/apps/backend/src/policies/message-policy.ts b/apps/backend/src/policies/message-policy.ts index 0db6e3a3d..7a0138d51 100644 --- a/apps/backend/src/policies/message-policy.ts +++ b/apps/backend/src/policies/message-policy.ts @@ -1,7 +1,7 @@ import { ChannelRepo, MessageRepo, OrganizationMemberRepo } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { ChannelId, MessageId } from "@hazel/schema" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" import { isAdminOrOwner, withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" @@ -86,10 +86,11 @@ export class MessagePolicy extends ServiceMap.Service()("MessageP return { canCreate, canRead, canUpdate, canDelete } as const }), - dependencies: [ - MessageRepo.Default, - ChannelRepo.Default, - OrganizationMemberRepo.Default, - OrgResolver.Default, - ], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(MessageRepo.Default), + Layer.provide(ChannelRepo.Default), + Layer.provide(OrganizationMemberRepo.Default), + Layer.provide(OrgResolver.Default), + ) +} diff --git a/apps/backend/src/policies/message-reaction-policy.ts b/apps/backend/src/policies/message-reaction-policy.ts index 12b228f51..8bf20a314 100644 --- a/apps/backend/src/policies/message-reaction-policy.ts +++ b/apps/backend/src/policies/message-reaction-policy.ts @@ -1,7 +1,7 @@ import { MessageReactionRepo, MessageRepo } from "@hazel/backend-core" import { ErrorUtils, PermissionError, policy } from "@hazel/domain" import type { MessageId, MessageReactionId } from "@hazel/schema" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { ConnectConversationService } from "../services/connect-conversation-service" import { OrgResolver } from "../services/org-resolver" @@ -99,11 +99,12 @@ export class MessageReactionPolicy extends ServiceMap.Service()("NotificationPolicy/Policy", { @@ -173,5 +173,9 @@ export class NotificationPolicy extends ServiceMap.Service() return { canCreate, canView, canUpdate, canDelete, canMarkAsRead, canMarkAllAsRead } as const }), - dependencies: [NotificationRepo.Default, OrganizationMemberRepo.Default], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(NotificationRepo.Default), + Layer.provide(OrganizationMemberRepo.Default), + ) +} diff --git a/apps/backend/src/policies/organization-member-policy.ts b/apps/backend/src/policies/organization-member-policy.ts index de7013b1f..f86f827b2 100644 --- a/apps/backend/src/policies/organization-member-policy.ts +++ b/apps/backend/src/policies/organization-member-policy.ts @@ -1,7 +1,7 @@ import { OrganizationMemberRepo } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { OrganizationId, OrganizationMemberId } from "@hazel/schema" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" import { OrgResolver } from "../services/org-resolver" export class OrganizationMemberPolicy extends ServiceMap.Service()( @@ -103,6 +103,10 @@ export class OrganizationMemberPolicy extends ServiceMap.Service() return { canUpdate, canDelete, canCreate, isMember, canManagePublicInvite } as const }), - dependencies: [OrgResolver.Default], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(OrgResolver.Default), + ) +} diff --git a/apps/backend/src/policies/pinned-message-policy.ts b/apps/backend/src/policies/pinned-message-policy.ts index 5585cf485..6978a1df6 100644 --- a/apps/backend/src/policies/pinned-message-policy.ts +++ b/apps/backend/src/policies/pinned-message-policy.ts @@ -1,7 +1,7 @@ import { ChannelRepo, OrganizationMemberRepo, PinnedMessageRepo } from "@hazel/backend-core" import { ErrorUtils, policy } from "@hazel/domain" import type { ChannelId, PinnedMessageId } from "@hazel/schema" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" import { isAdminOrOwner } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" @@ -112,10 +112,11 @@ export class PinnedMessagePolicy extends ServiceMap.Service return { canCreate, canDelete, canUpdate } as const }), - dependencies: [ - PinnedMessageRepo.Default, - ChannelRepo.Default, - OrganizationMemberRepo.Default, - OrgResolver.Default, - ], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(PinnedMessageRepo.Default), + Layer.provide(ChannelRepo.Default), + Layer.provide(OrganizationMemberRepo.Default), + Layer.provide(OrgResolver.Default), + ) +} diff --git a/apps/backend/src/policies/rss-subscription-policy.ts b/apps/backend/src/policies/rss-subscription-policy.ts index 8e4a64b46..e1fca63ee 100644 --- a/apps/backend/src/policies/rss-subscription-policy.ts +++ b/apps/backend/src/policies/rss-subscription-policy.ts @@ -1,7 +1,7 @@ import { ChannelRepo, RssSubscriptionRepo } from "@hazel/backend-core" import { ErrorUtils } from "@hazel/domain" import type { ChannelId, OrganizationId, RssSubscriptionId } from "@hazel/schema" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" import { withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" @@ -96,6 +96,11 @@ export class RssSubscriptionPolicy extends ServiceMap.Service()( @@ -52,6 +52,10 @@ export class TypingIndicatorPolicy extends ServiceMap.Service()("UserPolicy/Policy", { @@ -17,5 +17,6 @@ export class UserPolicy extends ServiceMap.Service()("UserPolicy/Pol return { canCreate, canUpdate, canDelete, canRead } as const }), - dependencies: [], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/apps/backend/src/policies/user-presence-status-policy.ts b/apps/backend/src/policies/user-presence-status-policy.ts index c99eb722e..b06ff7926 100644 --- a/apps/backend/src/policies/user-presence-status-policy.ts +++ b/apps/backend/src/policies/user-presence-status-policy.ts @@ -1,4 +1,4 @@ -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" import { makePolicy } from "../lib/policy-utils" export class UserPresenceStatusPolicy extends ServiceMap.Service()( @@ -18,6 +18,7 @@ export class UserPresenceStatusPolicy extends ServiceMap.Service "organization", }), /** Full URL to redirect after OAuth completes (e.g., http://localhost:3000/org/settings/integrations/github) */ returnTo: Schema.String, /** Environment that initiated the OAuth flow. Used to redirect back to localhost for local dev. */ - environment: Schema.optional(Schema.Literal("local", "production")), + environment: Schema.optional(Schema.Literals(["local", "production"])), }) /** @@ -414,11 +414,11 @@ const handleGetOAuthUrl = Effect.fn("integrations.getOAuthUrl")(function* ( const OAuthSessionState = Schema.Struct({ organizationId: Schema.String, userId: Schema.String, - level: Schema.optionalWith(Schema.Literal("organization", "user"), { + level: Schema.optionalWith(Schema.Literals(["organization", "user"]), { default: () => "organization", }), returnTo: Schema.String, - environment: Schema.optional(Schema.Literal("local", "production")), + environment: Schema.optional(Schema.Literals(["local", "production"])), createdAt: Schema.Number, }) diff --git a/apps/backend/src/rpc/middleware/auth-class.ts b/apps/backend/src/rpc/middleware/auth-class.ts index 57f1c2f45..88a03cf6a 100644 --- a/apps/backend/src/rpc/middleware/auth-class.ts +++ b/apps/backend/src/rpc/middleware/auth-class.ts @@ -45,7 +45,7 @@ import { Schema as S } from "effect" * }) * ``` */ -const AuthFailure = S.Union( +const AuthFailure = S.Union([ UnauthorizedError, SessionLoadError, SessionAuthenticationError, @@ -55,10 +55,11 @@ const AuthFailure = S.Union( SessionExpiredError, InvalidBearerTokenError, WorkOSUserFetchError, -) +]) -export class AuthMiddleware extends RpcMiddleware.Tag()("AuthMiddleware", { - provides: CurrentUser.Context, - failure: AuthFailure, +export class AuthMiddleware extends RpcMiddleware.Service()("AuthMiddleware", { + error: AuthFailure, requiredForClient: true, }) {} diff --git a/apps/backend/src/services/bot-gateway-service.ts b/apps/backend/src/services/bot-gateway-service.ts index f5145b784..7ffbadeaf 100644 --- a/apps/backend/src/services/bot-gateway-service.ts +++ b/apps/backend/src/services/bot-gateway-service.ts @@ -6,7 +6,7 @@ import { } from "@hazel/domain" import type { Channel, ChannelMember, Message } from "@hazel/domain/models" import type { BotId, ChannelId, OrganizationId } from "@hazel/schema" -import { ServiceMap, Config, Effect, Option, Ref, Schema } from "effect" +import { ServiceMap, Config, Effect, Layer, Option, Ref, Schema } from "effect" const DEFAULT_DURABLE_STREAMS_URL = "http://localhost:4437/v1/stream" @@ -29,7 +29,6 @@ export class DurableStreamRequestError extends Schema.TaggedErrorClass()("BotGatewayService", { - dependencies: [BotInstallationRepo.Default, ChannelRepo.Default], make: Effect.gen(function* () { const installationRepo = yield* BotInstallationRepo const channelRepo = yield* ChannelRepo @@ -280,4 +279,9 @@ export class BotGatewayService extends ServiceMap.Service()(" proxyRead, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(BotInstallationRepo.Default), + Layer.provide(ChannelRepo.Default), + ) +} diff --git a/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.ts b/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.ts index f558ccf1a..79735a61e 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.ts @@ -1,7 +1,7 @@ import { MessageRepo, OrganizationMemberRepo, UserRepo } from "@hazel/backend-core" import type { OrganizationId, UserId } from "@hazel/schema" import type { IntegrationConnection } from "@hazel/domain/models" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" interface ReconcileAttributionParams { organizationId: OrganizationId @@ -126,6 +126,11 @@ export class ChatSyncAttributionReconciler extends ServiceMap.Service() ingestThreadCreate, } }), - dependencies: [ - ChatSyncConnectionRepo.Default, - ChatSyncChannelLinkRepo.Default, - ChatSyncMessageLinkRepo.Default, - ChatSyncEventReceiptRepo.Default, - MessageRepo.Default, - MessageOutboxRepo.Default, - MessageReactionRepo.Default, - ChannelRepo.Default, - IntegrationConnectionRepo.Default, - UserRepo.Default, - OrganizationMemberRepo.Default, - IntegrationBotService.Default, - ChannelAccessSyncService.Default, - ChatSyncProviderRegistry.Default, - Discord.DiscordApiClient.Default, - ], -}) {} +}) { + static readonly layer = Layer.effect(this, this.effect).pipe( + Layer.provide(ChatSyncConnectionRepo.Default), + Layer.provide(ChatSyncChannelLinkRepo.Default), + Layer.provide(ChatSyncMessageLinkRepo.Default), + Layer.provide(ChatSyncEventReceiptRepo.Default), + Layer.provide(MessageRepo.Default), + Layer.provide(MessageOutboxRepo.Default), + Layer.provide(MessageReactionRepo.Default), + Layer.provide(ChannelRepo.Default), + Layer.provide(IntegrationConnectionRepo.Default), + Layer.provide(UserRepo.Default), + Layer.provide(OrganizationMemberRepo.Default), + Layer.provide(IntegrationBotService.Default), + Layer.provide(ChannelAccessSyncService.Default), + Layer.provide(ChatSyncProviderRegistry.Default), + Layer.provide(Discord.DiscordApiClient.Default), + ) +} diff --git a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts index 9a7841fc5..cc84908e7 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts @@ -1,5 +1,5 @@ import { Discord } from "@hazel/integrations" -import { ServiceMap, Config, Effect, Option, Redacted, Schema, Schedule } from "effect" +import { ServiceMap, Config, Effect, Layer, Option, Redacted, Schema, Schedule } from "effect" import { type ChatSyncOutboundAttachment, formatMessageContentWithAttachments, @@ -436,6 +436,9 @@ export class ChatSyncProviderRegistry extends ServiceMap.Service()(" ingestThreadCreate, } }), - dependencies: [ChatSyncCoreWorker.Default], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(ChatSyncCoreWorker.Default), + ) +} diff --git a/apps/backend/src/services/connect-conversation-service.ts b/apps/backend/src/services/connect-conversation-service.ts index 5ffcf8ed0..bd732033f 100644 --- a/apps/backend/src/services/connect-conversation-service.ts +++ b/apps/backend/src/services/connect-conversation-service.ts @@ -8,23 +8,13 @@ import { } from "@hazel/backend-core" import { InternalServerError } from "@hazel/domain" import type { ChannelId, ConnectConversationId, OrganizationId, UserId } from "@hazel/schema" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" import { ChannelAccessSyncService } from "./channel-access-sync" import { OrgResolver } from "./org-resolver" export class ConnectConversationService extends ServiceMap.Service()( "ConnectConversationService", { - dependencies: [ - ChannelRepo.Default, - ConnectParticipantRepo.Default, - ConnectConversationRepo.Default, - ConnectConversationChannelRepo.Default, - MessageRepo.Default, - MessageReactionRepo.Default, - ChannelAccessSyncService.Default, - OrgResolver.Default, - ], effect: Effect.gen(function* () { const channelRepo = yield* ChannelRepo const connectParticipantRepo = yield* ConnectParticipantRepo @@ -331,4 +321,15 @@ export class ConnectConversationService extends ServiceMap.Service()( "IntegrationEncryptionError", @@ -145,4 +145,6 @@ export class IntegrationEncryption extends ServiceMap.Service export class MessageOutboxDispatcher extends ServiceMap.Service()( "MessageOutboxDispatcher", { - dependencies: [EnvVars.Default, MessageOutboxRepo.Default, MessageSideEffectService.Default], effect: Effect.gen(function* () { const envVars = yield* EnvVars const database = yield* Database.Database @@ -235,4 +234,10 @@ export class MessageOutboxDispatcher extends ServiceMap.Service()( "MessageSideEffectService", { - dependencies: [DiscordSyncWorker.Default], effect: Effect.gen(function* () { const db = yield* Database.Database const discordSyncWorker = yield* DiscordSyncWorker @@ -287,4 +286,8 @@ export class MessageSideEffectService extends ServiceMap.Service()(" generateForMarketingScreenshots, } }), - dependencies: [DatabaseLive], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(DatabaseLive), + ) +} diff --git a/apps/backend/src/services/oauth/oauth-http-client.ts b/apps/backend/src/services/oauth/oauth-http-client.ts index f2c8bf48b..201df813f 100644 --- a/apps/backend/src/services/oauth/oauth-http-client.ts +++ b/apps/backend/src/services/oauth/oauth-http-client.ts @@ -6,7 +6,7 @@ */ import { FetchHttpClient, HttpBody, HttpClient } from "effect/unstable/http" -import { ServiceMap, Duration, Effect, Schema } from "effect" +import { ServiceMap, Duration, Effect, Layer, Schema } from "effect" import { TreeFormatter } from "effect/ParseResult" import type { OAuthIntegrationProvider } from "./provider-config" @@ -277,5 +277,8 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut refreshToken: wrappedRefreshToken, } }), - dependencies: [FetchHttpClient.layer], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(FetchHttpClient.layer), + ) +} diff --git a/apps/backend/src/services/oauth/oauth-provider-registry.ts b/apps/backend/src/services/oauth/oauth-provider-registry.ts index 79a731480..ae5f0d85a 100644 --- a/apps/backend/src/services/oauth/oauth-provider-registry.ts +++ b/apps/backend/src/services/oauth/oauth-provider-registry.ts @@ -1,6 +1,6 @@ import { UnsupportedProviderError } from "@hazel/domain/http" import { GitHub } from "@hazel/integrations" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" import type { OAuthProvider } from "./oauth-provider" import { ProviderNotConfiguredError } from "./oauth-provider" import type { IntegrationProvider, OAuthIntegrationProvider, OAuthProviderConfig } from "./provider-config" @@ -157,5 +157,9 @@ export class OAuthProviderRegistry extends ServiceMap.Service()("OrgResolver" checkChannelAccess, } as const }), - dependencies: [ - OrganizationMemberRepo.Default, - ChannelRepo.Default, - ChannelMemberRepo.Default, - MessageRepo.Default, - ], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(OrganizationMemberRepo.Default), + Layer.provide(ChannelRepo.Default), + Layer.provide(ChannelMemberRepo.Default), + Layer.provide(MessageRepo.Default), + ) +} diff --git a/apps/backend/src/services/rate-limiter.ts b/apps/backend/src/services/rate-limiter.ts index 88d41b653..a8d3c4d4f 100644 --- a/apps/backend/src/services/rate-limiter.ts +++ b/apps/backend/src/services/rate-limiter.ts @@ -59,7 +59,6 @@ end * Rate limiter service backed by Redis via @hazel/effect-bun */ export class RateLimiter extends ServiceMap.Service()("RateLimiter", { - dependencies: [Redis.Default], make: Effect.gen(function* () { const redis = yield* Redis @@ -98,7 +97,11 @@ export class RateLimiter extends ServiceMap.Service()("RateLimiter" ), } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(Redis.Default), + ) +} /** * In-memory rate limiter for testing (no Redis required) diff --git a/apps/backend/src/services/session-manager.ts b/apps/backend/src/services/session-manager.ts index 43f2498ee..7a124ee54 100644 --- a/apps/backend/src/services/session-manager.ts +++ b/apps/backend/src/services/session-manager.ts @@ -6,7 +6,7 @@ import { WorkOSUserFetchError, } from "@hazel/domain" import { UserRepo } from "@hazel/backend-core" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" /** * Session management service that handles authentication via WorkOS. @@ -15,7 +15,6 @@ import { ServiceMap, Effect } from "effect" * This service delegates to @hazel/auth/backend for the actual authentication logic. */ export class SessionManager extends ServiceMap.Service()("SessionManager", { - dependencies: [BackendAuth.Default, UserRepo.Default], make: Effect.gen(function* () { const auth = yield* BackendAuth const userRepo = yield* UserRepo @@ -37,4 +36,9 @@ export class SessionManager extends ServiceMap.Service()("Sessio >, } as const }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(BackendAuth.Default), + Layer.provide(UserRepo.Default), + ) +} diff --git a/apps/backend/src/services/webhook-bot-service.ts b/apps/backend/src/services/webhook-bot-service.ts index bbfef4367..2e088d0d3 100644 --- a/apps/backend/src/services/webhook-bot-service.ts +++ b/apps/backend/src/services/webhook-bot-service.ts @@ -1,6 +1,6 @@ import { OrganizationMemberRepo, UserRepo } from "@hazel/backend-core" import type { ChannelWebhookId, OrganizationId, UserId } from "@hazel/schema" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" /** * Webhook Bot Service @@ -66,5 +66,9 @@ export class WebhookBotService extends ServiceMap.Service()(" return { createWebhookBot, updateWebhookBot } }), - dependencies: [UserRepo.Default, OrganizationMemberRepo.Default], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(UserRepo.Default), + Layer.provide(OrganizationMemberRepo.Default), + ) +} diff --git a/apps/backend/src/services/workos-auth.ts b/apps/backend/src/services/workos-auth.ts index 6a31f0205..fb5956038 100644 --- a/apps/backend/src/services/workos-auth.ts +++ b/apps/backend/src/services/workos-auth.ts @@ -24,4 +24,6 @@ export class WorkOSAuth extends ServiceMap.Service()("WorkOSAuth", { call, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/apps/backend/src/services/workos-webhook.ts b/apps/backend/src/services/workos-webhook.ts index 49c80f1f7..85fcdf7e0 100644 --- a/apps/backend/src/services/workos-webhook.ts +++ b/apps/backend/src/services/workos-webhook.ts @@ -158,4 +158,6 @@ export class WorkOSWebhookVerifier extends ServiceMap.Service()("GatewayC }) return config }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} class GatewayAuthError extends Schema.TaggedErrorClass()("GatewayAuthError", { message: Schema.String, @@ -157,7 +160,7 @@ class GatewayProtocolError extends Schema.TaggedErrorClass }) {} export class GatewayStartupError extends Schema.TaggedErrorClass()("GatewayStartupError", { - dependency: Schema.Literal("config", "database", "redis", "tracer", "server"), + dependency: Schema.Literals(["config", "database", "redis", "tracer", "server"]), message: Schema.String, cause: Schema.optional(Schema.Unknown), }) {} @@ -320,7 +323,9 @@ class DurableStreamClient extends ServiceMap.Service()("Dur readBatch, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} class BotGatewayHub extends ServiceMap.Service()("BotGatewayHub", { make: Effect.gen(function* () { @@ -758,7 +763,9 @@ class BotGatewayHub extends ServiceMap.Service()("BotGatewayHub", proxyRead: durableStreams.proxyRead, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} const DatabaseLive = Layer.unwrapEffect( Effect.gen(function* () { diff --git a/apps/cluster/src/services/bot-user-service.ts b/apps/cluster/src/services/bot-user-service.ts index 38bd61c9a..25c1d4341 100644 --- a/apps/cluster/src/services/bot-user-service.ts +++ b/apps/cluster/src/services/bot-user-service.ts @@ -116,7 +116,9 @@ export class BotUserService extends ServiceMap.Service()("BotUse warmCache, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} /** * Layer that provides BotUserService with Database dependency. diff --git a/apps/cluster/src/workflows/github-webhook-handler.ts b/apps/cluster/src/workflows/github-webhook-handler.ts index 52463320a..c40d2e8eb 100644 --- a/apps/cluster/src/workflows/github-webhook-handler.ts +++ b/apps/cluster/src/workflows/github-webhook-handler.ts @@ -127,7 +127,7 @@ export const GitHubWebhookWorkflowLayer = Cluster.GitHubWebhookWorkflow.toLayer( const messagesResult = yield* Activity.make({ name: "CreateGitHubMessages", success: Cluster.CreateGitHubMessagesResult, - error: Schema.Union(Cluster.CreateGitHubMessageError, Cluster.BotUserQueryError), + error: Schema.Union([Cluster.CreateGitHubMessageError, Cluster.BotUserQueryError]), execute: Effect.gen(function* () { const db = yield* Database.Database const botUserService = yield* BotUserService diff --git a/apps/cluster/src/workflows/message-notification-handler.ts b/apps/cluster/src/workflows/message-notification-handler.ts index ca7a8a52b..2a876c79a 100644 --- a/apps/cluster/src/workflows/message-notification-handler.ts +++ b/apps/cluster/src/workflows/message-notification-handler.ts @@ -300,7 +300,7 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf const notificationsResult = yield* Activity.make({ name: "CreateNotifications", success: Cluster.CreateNotificationsResult, - error: Schema.Union(Cluster.CreateNotificationError), + error: Schema.Union([Cluster.CreateNotificationError]), execute: Effect.gen(function* () { const db = yield* Database.Database const startedAt = Date.now() diff --git a/apps/cluster/src/workflows/rss-feed-poll-handler.ts b/apps/cluster/src/workflows/rss-feed-poll-handler.ts index 701d3c7b0..64156d20d 100644 --- a/apps/cluster/src/workflows/rss-feed-poll-handler.ts +++ b/apps/cluster/src/workflows/rss-feed-poll-handler.ts @@ -111,7 +111,7 @@ export const RssFeedPollWorkflowLayer = Cluster.RssFeedPollWorkflow.toLayer( const postResult = yield* Activity.make({ name: "FilterAndPostItems", success: Cluster.PostRssItemsResult, - error: Schema.Union(Cluster.PostRssItemsError, Cluster.BotUserQueryError), + error: Schema.Union([Cluster.PostRssItemsError, Cluster.BotUserQueryError]), execute: Effect.gen(function* () { const db = yield* Database.Database const botUserService = yield* BotUserService diff --git a/apps/electric-proxy/src/cache/access-context-cache.ts b/apps/electric-proxy/src/cache/access-context-cache.ts index a30c51109..848d44b43 100644 --- a/apps/electric-proxy/src/cache/access-context-cache.ts +++ b/apps/electric-proxy/src/cache/access-context-cache.ts @@ -29,7 +29,7 @@ export class AccessContextLookupError extends Schema.TaggedErrorClass() redisUrl, } satisfies ProxyConfig }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/apps/link-preview-worker/src/api.ts b/apps/link-preview-worker/src/api.ts index bcf30fcdb..6a6e6b537 100644 --- a/apps/link-preview-worker/src/api.ts +++ b/apps/link-preview-worker/src/api.ts @@ -6,7 +6,7 @@ export class LinkPreviewApi extends HttpApi.make("api") .add(LinkPreviewGroup) .add(TweetGroup) .annotateContext( - OpenApi.annotations({ + OpenApi.annotate({ title: "Link Preview Worker API", description: "API for fetching link previews and tweet data", }), diff --git a/apps/link-preview-worker/src/declare.ts b/apps/link-preview-worker/src/declare.ts index db61b1767..f1da30e74 100644 --- a/apps/link-preview-worker/src/declare.ts +++ b/apps/link-preview-worker/src/declare.ts @@ -6,7 +6,7 @@ export class AppApi extends HttpApiGroup.make("app") .add(HttpApiEndpoint.get("health", "/health").addSuccess(Schema.String)) .annotateContext( - OpenApi.annotations({ + OpenApi.annotate({ title: "App Api", description: "App Api", }), @@ -41,7 +41,7 @@ export class LinkPreviewGroup extends HttpApiGroup.make("linkPreview") }), ) .annotateContext( - OpenApi.annotations({ + OpenApi.annotate({ title: "Get Link Preview", description: "Fetch metadata for a given URL", summary: "Get link preview metadata", @@ -70,7 +70,7 @@ export class TweetGroup extends HttpApiGroup.make("tweet") }), ) .annotateContext( - OpenApi.annotations({ + OpenApi.annotate({ title: "Get Tweet", description: "Fetch tweet data by ID", summary: "Get tweet metadata", diff --git a/apps/link-preview-worker/src/services/twitter.ts b/apps/link-preview-worker/src/services/twitter.ts index b45413d96..6addeb4a3 100644 --- a/apps/link-preview-worker/src/services/twitter.ts +++ b/apps/link-preview-worker/src/services/twitter.ts @@ -1,5 +1,5 @@ import { FetchHttpClient, HttpClient } from "effect/unstable/http" -import { ServiceMap, Effect, Schema } from "effect" +import { ServiceMap, Effect, Layer, Schema } from "effect" const SYNDICATION_URL = "https://cdn.syndication.twimg.com" const TWEET_ID_REGEX = /^[0-9]+$/ @@ -167,5 +167,8 @@ export class TwitterApi extends ServiceMap.Service()("TwitterApi", { }), } }), - dependencies: [FetchHttpClient.layer], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(FetchHttpClient.layer), + ) +} diff --git a/apps/web/src/atoms/notification-sound-atoms.ts b/apps/web/src/atoms/notification-sound-atoms.ts index d82d6cc93..6dc89ed59 100644 --- a/apps/web/src/atoms/notification-sound-atoms.ts +++ b/apps/web/src/atoms/notification-sound-atoms.ts @@ -26,7 +26,7 @@ export interface NotificationSoundSettings { const NotificationSoundSettingsSchema = Schema.Struct({ enabled: Schema.Boolean, volume: Schema.Number, - soundFile: Schema.Literal("notification01", "notification03"), + soundFile: Schema.Literals(["notification01", "notification03"]), cooldownMs: Schema.Number, }) diff --git a/apps/web/src/atoms/search-atoms.ts b/apps/web/src/atoms/search-atoms.ts index 493df807e..6d7580dd5 100644 --- a/apps/web/src/atoms/search-atoms.ts +++ b/apps/web/src/atoms/search-atoms.ts @@ -8,7 +8,7 @@ export const MAX_RECENT_SEARCHES = 10 * Schema for a resolved search filter */ const SearchFilterSchema = Schema.Struct({ - type: Schema.Literal("from", "in", "has", "before", "after"), + type: Schema.Literals(["from", "in", "has", "before", "after"]), value: Schema.String, displayValue: Schema.String, id: Schema.String, diff --git a/apps/web/src/lib/error-messages.ts b/apps/web/src/lib/error-messages.ts index 84b5466a3..1f445f813 100644 --- a/apps/web/src/lib/error-messages.ts +++ b/apps/web/src/lib/error-messages.ts @@ -60,7 +60,7 @@ export interface UserErrorMessage { * Schema union for Schema-based common errors. * Used for type-safe error matching and runtime validation. */ -export const CommonAppErrorSchema = Schema.Union( +export const CommonAppErrorSchema = Schema.Union([ // Auth errors (401) UnauthorizedError, SessionNotProvidedError, @@ -111,7 +111,7 @@ export const CommonAppErrorSchema = Schema.Union( AIRateLimitError, AIResponseParseError, ThreadNameUpdateError, -) +]) /** * Union of common application errors that have user-friendly messages. diff --git a/apps/web/src/lib/services/common/api-client.ts b/apps/web/src/lib/services/common/api-client.ts index f3f38efc9..65656f464 100644 --- a/apps/web/src/lib/services/common/api-client.ts +++ b/apps/web/src/lib/services/common/api-client.ts @@ -17,7 +17,6 @@ export const CustomFetchLive = FetchHttpClient.layer.pipe( ) export class ApiClient extends ServiceMap.Service()("ApiClient", { - dependencies: [CustomFetchLive], make: Effect.gen(function* () { return yield* HttpApiClient.make(HazelApi, { baseUrl: import.meta.env.VITE_BACKEND_URL, @@ -40,4 +39,8 @@ export class ApiClient extends ServiceMap.Service()("ApiClient", { ), }) }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(CustomFetchLive), + ) +} diff --git a/apps/web/src/lib/services/common/network-mode.ts b/apps/web/src/lib/services/common/network-mode.ts index 3141e5f58..e34f3ce6e 100644 --- a/apps/web/src/lib/services/common/network-mode.ts +++ b/apps/web/src/lib/services/common/network-mode.ts @@ -1,5 +1,6 @@ import * as Chunk from "effect/Chunk" import * as Effect from "effect/Effect" +import * as ServiceMap from "effect/ServiceMap" import * as Stream from "effect/Stream" import * as SubscriptionRef from "effect/SubscriptionRef" @@ -25,4 +26,6 @@ export class NetworkMonitor extends ServiceMap.Service()("Networ return { latch, ref } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/apps/web/src/lib/services/desktop/tauri-auth.ts b/apps/web/src/lib/services/desktop/tauri-auth.ts index 1ebb037f0..38cc9ad71 100644 --- a/apps/web/src/lib/services/desktop/tauri-auth.ts +++ b/apps/web/src/lib/services/desktop/tauri-auth.ts @@ -22,7 +22,7 @@ import { TauriCommandError, TauriNotAvailableError, } from "@hazel/domain/errors" -import { ServiceMap, Deferred, Duration, Effect, FiberId } from "effect" +import { ServiceMap, Deferred, Duration, Effect, FiberId, Layer } from "effect" import { TokenExchange } from "./token-exchange" import { TokenStorage } from "./token-storage" @@ -83,7 +83,6 @@ const getTauriEvent = Effect.gen(function* () { }) export class TauriAuth extends ServiceMap.Service()("TauriAuth", { - dependencies: [TokenStorage.Default, TokenExchange.Default], make: Effect.gen(function* () { const tokenStorage = yield* TokenStorage const tokenExchange = yield* TokenExchange @@ -205,4 +204,9 @@ export class TauriAuth extends ServiceMap.Service()("TauriAuth", { }).pipe(Effect.withSpan("TauriAuth.initiateAuth")), } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(TokenStorage.Default), + Layer.provide(TokenExchange.Default), + ) +} diff --git a/apps/web/src/lib/services/desktop/token-exchange.ts b/apps/web/src/lib/services/desktop/token-exchange.ts index 9d26aa64b..9613cb098 100644 --- a/apps/web/src/lib/services/desktop/token-exchange.ts +++ b/apps/web/src/lib/services/desktop/token-exchange.ts @@ -7,12 +7,11 @@ import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http" import { OAuthCodeExpiredError, TokenDecodeError, TokenExchangeError } from "@hazel/domain/errors" import { RefreshTokenResponse, TokenResponse } from "@hazel/domain/http" -import { ServiceMap, Duration, Effect, Schema } from "effect" +import { ServiceMap, Duration, Effect, Layer, Schema } from "effect" const DEFAULT_TIMEOUT = Duration.seconds(60) export class TokenExchange extends ServiceMap.Service()("TokenExchange", { - dependencies: [FetchHttpClient.layer], make: Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient const backendUrl = import.meta.env.VITE_BACKEND_URL @@ -172,6 +171,10 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc } }), }) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(FetchHttpClient.layer), + ) + /** * Mock token response for testing */ diff --git a/bots/hazel-bot/src/tools/base.ts b/bots/hazel-bot/src/tools/base.ts index 409de4eff..ab08bb129 100644 --- a/bots/hazel-bot/src/tools/base.ts +++ b/bots/hazel-bot/src/tools/base.ts @@ -9,11 +9,11 @@ export const GetCurrentTime = Tool.make("get_current_time", { export const Calculate = Tool.make("calculate", { description: "Perform basic arithmetic calculations", parameters: { - operation: Schema.Literal("add", "subtract", "multiply", "divide").annotations({ + operation: Schema.Literals(["add", "subtract", "multiply", "divide"]).annotate({ description: "The arithmetic operation to perform", }), - a: Schema.Number.annotations({ description: "First operand" }), - b: Schema.Number.annotations({ description: "Second operand" }), + a: Schema.Number.annotate({ description: "First operand" }), + b: Schema.Number.annotate({ description: "Second operand" }), }, success: Schema.Number, }) diff --git a/bots/hazel-bot/src/tools/craft.ts b/bots/hazel-bot/src/tools/craft.ts index 3f39dd0eb..16a7fdb79 100644 --- a/bots/hazel-bot/src/tools/craft.ts +++ b/bots/hazel-bot/src/tools/craft.ts @@ -4,7 +4,7 @@ import { Schema } from "effect" export const CraftSearchDocuments = Tool.make("craft_search_documents", { description: "Search across all documents in the connected Craft space", parameters: { - query: Schema.String.annotations({ description: "Search query text" }), + query: Schema.String.annotate({ description: "Search query text" }), }, success: Schema.Unknown, }) @@ -12,7 +12,7 @@ export const CraftSearchDocuments = Tool.make("craft_search_documents", { export const CraftGetDocument = Tool.make("craft_get_document", { description: "Fetch the content blocks of a Craft document by its ID", parameters: { - documentId: Schema.String.annotations({ description: "The document ID to fetch" }), + documentId: Schema.String.annotate({ description: "The document ID to fetch" }), }, success: Schema.Unknown, }) @@ -20,12 +20,12 @@ export const CraftGetDocument = Tool.make("craft_get_document", { export const CraftCreateDocument = Tool.make("craft_create_document", { description: "Create a new Craft document. Use this after confirming with the user what you will create.", parameters: { - title: Schema.String.annotations({ description: "Document title" }), + title: Schema.String.annotate({ description: "Document title" }), content: Schema.optional( - Schema.String.annotations({ description: "Initial text content for the document" }), + Schema.String.annotate({ description: "Initial text content for the document" }), ), folderId: Schema.optional( - Schema.String.annotations({ description: "Optional folder ID to create the document in" }), + Schema.String.annotate({ description: "Optional folder ID to create the document in" }), ), }, success: Schema.Unknown, @@ -34,17 +34,17 @@ export const CraftCreateDocument = Tool.make("craft_create_document", { export const CraftInsertBlocks = Tool.make("craft_insert_blocks", { description: "Add content blocks to an existing Craft document", parameters: { - documentId: Schema.String.annotations({ description: "The document ID to add blocks to" }), + documentId: Schema.String.annotate({ description: "The document ID to add blocks to" }), blocks: Schema.Array( Schema.Struct({ - type: Schema.String.annotations({ + type: Schema.String.annotate({ description: 'Block type (e.g., "text")', }), - content: Schema.optional(Schema.String.annotations({ description: "Block text content" })), + content: Schema.optional(Schema.String.annotate({ description: "Block text content" })), }), - ).annotations({ description: "Array of blocks to insert" }), + ).annotate({ description: "Array of blocks to insert" }), parentBlockId: Schema.optional( - Schema.String.annotations({ description: "Optional parent block ID to nest under" }), + Schema.String.annotate({ description: "Optional parent block ID to nest under" }), ), }, success: Schema.Unknown, @@ -54,7 +54,7 @@ export const CraftGetTasks = Tool.make("craft_get_tasks", { description: "List tasks from the connected Craft space", parameters: { scope: Schema.optional( - Schema.Literal("inbox", "active", "upcoming", "logbook").annotations({ + Schema.Literals(["inbox", "active", "upcoming", "logbook"]).annotate({ description: "Task scope filter (inbox, active, upcoming, or logbook)", }), ), @@ -66,9 +66,9 @@ export const CraftCreateTask = Tool.make("craft_create_task", { description: "Create a task in the connected Craft space. Use this after confirming with the user what you will create.", parameters: { - content: Schema.String.annotations({ description: "Task content/description" }), + content: Schema.String.annotate({ description: "Task content/description" }), documentId: Schema.optional( - Schema.String.annotations({ description: "Optional document ID to associate the task with" }), + Schema.String.annotate({ description: "Optional document ID to associate the task with" }), ), }, success: Schema.Unknown, @@ -82,8 +82,8 @@ export const CraftGetFolders = Tool.make("craft_get_folders", { export const CraftSearchBlocks = Tool.make("craft_search_blocks", { description: "Search within a specific Craft document for matching blocks", parameters: { - documentId: Schema.String.annotations({ description: "The document ID to search within" }), - query: Schema.String.annotations({ description: "Search query text" }), + documentId: Schema.String.annotate({ description: "The document ID to search within" }), + query: Schema.String.annotate({ description: "Search query text" }), }, success: Schema.Unknown, }) diff --git a/bots/hazel-bot/src/tools/linear.ts b/bots/hazel-bot/src/tools/linear.ts index 4a264b26f..7f1da0732 100644 --- a/bots/hazel-bot/src/tools/linear.ts +++ b/bots/hazel-bot/src/tools/linear.ts @@ -19,14 +19,14 @@ export const LinearGetDefaultTeam = Tool.make("linear_get_default_team", { export const LinearCreateIssue = Tool.make("linear_create_issue", { description: "Create a Linear issue. Use this after confirming with the user what you will create.", parameters: { - title: Schema.String.annotations({ + title: Schema.String.annotate({ description: "Issue title (max ~80 chars recommended)", }), description: Schema.optional( - Schema.String.annotations({ description: "Markdown description for the issue" }), + Schema.String.annotate({ description: "Markdown description for the issue" }), ), teamId: Schema.optional( - Schema.String.annotations({ + Schema.String.annotate({ description: "Optional team ID; if omitted, uses the user's default team", }), ), @@ -37,7 +37,7 @@ export const LinearCreateIssue = Tool.make("linear_create_issue", { export const LinearFetchIssue = Tool.make("linear_fetch_issue", { description: 'Fetch a Linear issue by key (e.g. "ENG-123")', parameters: { - issueKey: Schema.String.annotations({ description: 'Issue key like "ENG-123"' }), + issueKey: Schema.String.annotate({ description: 'Issue key like "ENG-123"' }), }, success: Schema.Struct({ issue: Schema.Unknown }), }) @@ -46,24 +46,24 @@ export const LinearListIssues = Tool.make("linear_list_issues", { description: "List Linear issues with optional filters (team, state, assignee, priority). Returns paginated results.", parameters: { - teamId: Schema.optional(Schema.String.annotations({ description: "Filter by team ID" })), + teamId: Schema.optional(Schema.String.annotate({ description: "Filter by team ID" })), stateType: Schema.optional( - Schema.Literal("triage", "backlog", "unstarted", "started", "completed", "canceled").annotations({ + Schema.Literals(["triage", "backlog", "unstarted", "started", "completed", "canceled"]).annotate({ description: "Filter by state type", }), ), - assigneeId: Schema.optional(Schema.String.annotations({ description: "Filter by assignee ID" })), + assigneeId: Schema.optional(Schema.String.annotate({ description: "Filter by assignee ID" })), priority: Schema.optional( - Schema.Number.annotations({ + Schema.Number.annotate({ description: "Filter by priority (0=None, 1=Urgent, 2=High, 3=Medium, 4=Low)", }), ), first: Schema.optional( - Schema.Number.annotations({ + Schema.Number.annotate({ description: "Number of issues to return (default 25, max 50)", }), ), - after: Schema.optional(Schema.String.annotations({ description: "Pagination cursor for next page" })), + after: Schema.optional(Schema.String.annotate({ description: "Pagination cursor for next page" })), }, success: Schema.Unknown, }) @@ -71,15 +71,15 @@ export const LinearListIssues = Tool.make("linear_list_issues", { export const LinearSearchIssues = Tool.make("linear_search_issues", { description: "Search Linear issues by text query. Searches across title, description, and comments.", parameters: { - query: Schema.String.annotations({ description: "Search text to find issues" }), + query: Schema.String.annotate({ description: "Search text to find issues" }), first: Schema.optional( - Schema.Number.annotations({ + Schema.Number.annotate({ description: "Number of issues to return (default 25, max 50)", }), ), - after: Schema.optional(Schema.String.annotations({ description: "Pagination cursor for next page" })), + after: Schema.optional(Schema.String.annotate({ description: "Pagination cursor for next page" })), includeArchived: Schema.optional( - Schema.Boolean.annotations({ + Schema.Boolean.annotate({ description: "Include archived issues in search (default false)", }), ), @@ -96,7 +96,7 @@ export const LinearGetWorkflowStates = Tool.make("linear_get_workflow_states", { description: "Get available workflow states (statuses) from Linear. Optionally filter by team. Use this to find valid state IDs before updating issues.", parameters: { - teamId: Schema.optional(Schema.String.annotations({ description: "Filter states by team ID" })), + teamId: Schema.optional(Schema.String.annotate({ description: "Filter states by team ID" })), }, success: Schema.Unknown, }) @@ -105,23 +105,23 @@ export const LinearUpdateIssue = Tool.make("linear_update_issue", { description: "Update an existing Linear issue. Use this after confirming with the user what changes to make. First use linear_get_workflow_states to get valid state IDs if changing status.", parameters: { - issueId: Schema.String.annotations({ + issueId: Schema.String.annotate({ description: 'Issue identifier (e.g., "ENG-123" or UUID)', }), - title: Schema.optional(Schema.String.annotations({ description: "New title for the issue" })), - description: Schema.optional(Schema.String.annotations({ description: "New markdown description" })), + title: Schema.optional(Schema.String.annotate({ description: "New title for the issue" })), + description: Schema.optional(Schema.String.annotate({ description: "New markdown description" })), stateId: Schema.optional( - Schema.String.annotations({ + Schema.String.annotate({ description: "New state/status ID (get valid IDs from linear_get_workflow_states)", }), ), assigneeId: Schema.optional( - Schema.NullOr(Schema.String).annotations({ + Schema.NullOr(Schema.String).annotate({ description: "New assignee ID, or null to unassign", }), ), priority: Schema.optional( - Schema.Number.annotations({ + Schema.Number.annotate({ description: "New priority (0=None, 1=Urgent, 2=High, 3=Medium, 4=Low)", }), ), diff --git a/libs/ai-openrouter/src/OpenRouterClient.ts b/libs/ai-openrouter/src/OpenRouterClient.ts index 0daf34047..8749b3c11 100644 --- a/libs/ai-openrouter/src/OpenRouterClient.ts +++ b/libs/ai-openrouter/src/OpenRouterClient.ts @@ -8,12 +8,12 @@ import * as HttpClient from "@effect/platform/HttpClient" import * as HttpClientRequest from "@effect/platform/HttpClientRequest" import * as Config from "effect/Config" import type { ConfigError } from "effect/ConfigError" -import * as Context from "effect/Context" import * as Effect from "effect/Effect" import { identity } from "effect/Function" import * as Layer from "effect/Layer" import type * as Redacted from "effect/Redacted" import * as Schema from "effect/Schema" +import * as ServiceMap from "effect/ServiceMap" import * as Stream from "effect/Stream" import * as Generated from "./Generated.js" import { OpenRouterConfig } from "./OpenRouterConfig.js" @@ -22,10 +22,9 @@ import { OpenRouterConfig } from "./OpenRouterConfig.js" * @since 1.0.0 * @category Context */ -export class OpenRouterClient extends Context.Tag("@effect/ai-openrouter/OpenRouterClient")< - OpenRouterClient, +export class OpenRouterClient extends ServiceMap.Service() {} +>()("@effect/ai-openrouter/OpenRouterClient") {} /** * @since 1.0.0 diff --git a/libs/ai-openrouter/src/OpenRouterConfig.ts b/libs/ai-openrouter/src/OpenRouterConfig.ts index 39c5d5bfa..3c9552122 100644 --- a/libs/ai-openrouter/src/OpenRouterConfig.ts +++ b/libs/ai-openrouter/src/OpenRouterConfig.ts @@ -2,18 +2,17 @@ * @since 1.0.0 */ import type { HttpClient } from "@effect/platform/HttpClient" -import * as Context from "effect/Context" import * as Effect from "effect/Effect" import { dual } from "effect/Function" +import * as ServiceMap from "effect/ServiceMap" /** * @since 1.0.0 * @category Context */ -export class OpenRouterConfig extends Context.Tag("@effect/ai-openrouter/OpenRouterConfig")< - OpenRouterConfig, +export class OpenRouterConfig extends ServiceMap.Service() { +>()("@effect/ai-openrouter/OpenRouterConfig") { /** * @since 1.0.0 */ diff --git a/libs/ai-openrouter/src/OpenRouterLanguageModel.ts b/libs/ai-openrouter/src/OpenRouterLanguageModel.ts index a4fe7862e..c095d06b6 100644 --- a/libs/ai-openrouter/src/OpenRouterLanguageModel.ts +++ b/libs/ai-openrouter/src/OpenRouterLanguageModel.ts @@ -9,13 +9,13 @@ import type * as Response from "@effect/ai/Response" import { addGenAIAnnotations } from "@effect/ai/Telemetry" import * as Tool from "@effect/ai/Tool" import * as Arr from "effect/Array" -import * as Context from "effect/Context" import * as DateTime from "effect/DateTime" import * as Effect from "effect/Effect" import * as Encoding from "effect/Encoding" import { dual } from "effect/Function" import * as Layer from "effect/Layer" import * as Predicate from "effect/Predicate" +import * as ServiceMap from "effect/ServiceMap" import * as Stream from "effect/Stream" import type { Span } from "effect/Tracer" import type { Simplify } from "effect/Types" @@ -32,10 +32,9 @@ import { OpenRouterClient } from "./OpenRouterClient.js" * @since 1.0.0 * @category Context */ -export class Config extends Context.Tag("@effect/ai-openrouter/OpenRouterLanguageModel/Config")< - Config, +export class Config extends ServiceMap.Service() { +>()("@effect/ai-openrouter/OpenRouterLanguageModel/Config") { /** * @since 1.0.0 */ diff --git a/libs/bot-sdk/src/auth.ts b/libs/bot-sdk/src/auth.ts index 3e371de2d..b76f6990c 100644 --- a/libs/bot-sdk/src/auth.ts +++ b/libs/bot-sdk/src/auth.ts @@ -56,7 +56,9 @@ export class BotAuth extends ServiceMap.Service()("BotAuth", { }), } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} /** * Helper to create auth context from bot token by calling the backend API diff --git a/libs/bot-sdk/src/hazel-bot-sdk.ts b/libs/bot-sdk/src/hazel-bot-sdk.ts index 52db6b616..f0622adc0 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.ts @@ -39,7 +39,6 @@ import { createTracingLayer } from "@hazel/effect-bun/Telemetry" import { Cache, Config, - Context, Duration, Effect, Layer, @@ -51,6 +50,7 @@ import { Ref, Runtime, Schema, + ServiceMap, } from "effect" import { BotAuth, createAuthContextFromToken } from "./auth.ts" import { createLoggerLayer, logLevelFromString, type BotLogConfig, type LogFormat } from "./log-config.ts" @@ -119,10 +119,9 @@ export interface HazelBotRuntimeConfig = Comm readonly heartbeatIntervalMs?: number } -export class HazelBotRuntimeConfigTag extends Context.Tag("@hazel/bot-sdk/HazelBotRuntimeConfig")< - HazelBotRuntimeConfigTag, +export class HazelBotRuntimeConfigTag extends ServiceMap.Service() {} +>()("@hazel/bot-sdk/HazelBotRuntimeConfig") {} /** * Hazel-specific type aliases for convenience @@ -1777,7 +1776,9 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB }, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} /** * Configuration for creating a Hazel bot diff --git a/libs/bot-sdk/src/services/health-server.ts b/libs/bot-sdk/src/services/health-server.ts index 96db22b44..18ab22734 100644 --- a/libs/bot-sdk/src/services/health-server.ts +++ b/libs/bot-sdk/src/services/health-server.ts @@ -72,7 +72,9 @@ export class BotHealthServer extends ServiceMap.Service()("BotH return { port: server.port } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} export const BotHealthServerLive = (port: number) => Layer.provide(BotHealthServer.Default, Layer.succeed(BotHealthServerConfigTag, { port })) diff --git a/libs/bot-sdk/src/streaming/actors-client.ts b/libs/bot-sdk/src/streaming/actors-client.ts index 8249f7c9f..eed7fb2f7 100644 --- a/libs/bot-sdk/src/streaming/actors-client.ts +++ b/libs/bot-sdk/src/streaming/actors-client.ts @@ -93,4 +93,6 @@ export class ActorsClient extends ServiceMap.Service()("@hazel/bot botToken: config.botToken, } as ActorsClientService }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/libs/bot-sdk/src/streaming/types.ts b/libs/bot-sdk/src/streaming/types.ts index ee51d4fa2..59385e9c6 100644 --- a/libs/bot-sdk/src/streaming/types.ts +++ b/libs/bot-sdk/src/streaming/types.ts @@ -168,7 +168,7 @@ export type ToolResultChunk = Schema.Schema.Type * } * ``` */ -export const AIContentChunk = Schema.Union(TextChunk, ThinkingChunk, ToolCallChunk, ToolResultChunk) +export const AIContentChunk = Schema.Union([TextChunk, ThinkingChunk, ToolCallChunk, ToolResultChunk]) export type AIContentChunk = Schema.Schema.Type /** diff --git a/libs/effect-electric-db-collection/src/errors.ts b/libs/effect-electric-db-collection/src/errors.ts index ca5f8efa5..9f6bf5324 100644 --- a/libs/effect-electric-db-collection/src/errors.ts +++ b/libs/effect-electric-db-collection/src/errors.ts @@ -52,7 +52,7 @@ export class TxIdTimeoutError extends Schema.TaggedErrorClass( */ export class MissingTxIdError extends Schema.TaggedErrorClass()("MissingTxIdError", { message: Schema.String, - operation: Schema.Literal("insert", "update", "delete"), + operation: Schema.Literals(["insert", "update", "delete"]), }) {} /** diff --git a/libs/effect-electric-db-collection/src/tanstack-errors.ts b/libs/effect-electric-db-collection/src/tanstack-errors.ts index ff0667d32..009c3f515 100644 --- a/libs/effect-electric-db-collection/src/tanstack-errors.ts +++ b/libs/effect-electric-db-collection/src/tanstack-errors.ts @@ -5,7 +5,7 @@ import { Schema } from "effect" */ export const ValidationIssue = Schema.Struct({ message: Schema.String, - path: Schema.optional(Schema.Array(Schema.Union(Schema.String, Schema.Number))), + path: Schema.optional(Schema.Array(Schema.Union([Schema.String, Schema.Number]))), }) export type ValidationIssue = typeof ValidationIssue.Type @@ -25,7 +25,7 @@ export class DuplicateKeyEffectError extends Schema.TaggedErrorClass()("JwksService", { - dependencies: [TokenValidationConfigService.Default], make: Effect.gen(function* () { const config = yield* TokenValidationConfigService const jwksRef = yield* Ref.make>(Option.none()) @@ -40,4 +39,8 @@ export class JwksService extends ServiceMap.Service()("JwksService" getJwks, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(TokenValidationConfigService.Default), + ) +} diff --git a/packages/actors/src/auth/token-validation-service.ts b/packages/actors/src/auth/token-validation-service.ts index f3962ef70..7d806c7fb 100644 --- a/packages/actors/src/auth/token-validation-service.ts +++ b/packages/actors/src/auth/token-validation-service.ts @@ -1,6 +1,6 @@ import { HttpClient, HttpClientRequest } from "effect/unstable/http" import { WorkOSJwtClaims, WorkOSRole } from "@hazel/schema" -import { ServiceMap, Either, Effect, Option, Redacted, Schema } from "effect" +import { ServiceMap, Either, Effect, Layer, Option, Redacted, Schema } from "effect" import { TreeFormatter } from "effect/ParseResult" import type { JWTPayload } from "jose" import { jwtVerify } from "jose" @@ -42,7 +42,6 @@ function isBotToken(token: string): boolean { export class TokenValidationService extends ServiceMap.Service()( "TokenValidationService", { - dependencies: [TokenValidationConfigService.Default, JwksService.Default], effect: Effect.gen(function* () { const config = yield* TokenValidationConfigService const jwksService = yield* JwksService @@ -242,7 +241,12 @@ export class TokenValidationService extends ServiceMap.Service * This is used by the backend HTTP API and WebSocket RPC handlers. */ export class BackendAuth extends ServiceMap.Service()("@hazel/auth/BackendAuth", { - dependencies: [WorkOSClient.Default], make: Effect.gen(function* () { const workos = yield* WorkOSClient const clientId = yield* Config.string("WORKOS_CLIENT_ID").pipe(Effect.orDie) @@ -352,6 +351,10 @@ export class BackendAuth extends ServiceMap.Service()("@hazel/auth/ } }), }) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(WorkOSClient.Default), + ) + /** Mock user ID - a valid UUID */ static readonly mockUserId = "00000000-0000-0000-0000-000000000001" as UserId diff --git a/packages/auth/src/consumers/proxy-auth.ts b/packages/auth/src/consumers/proxy-auth.ts index 61248552b..425f10efa 100644 --- a/packages/auth/src/consumers/proxy-auth.ts +++ b/packages/auth/src/consumers/proxy-auth.ts @@ -6,7 +6,7 @@ import { type WorkOSOrganizationId, type WorkOSUserId, } from "@hazel/schema" -import { ServiceMap, Effect, Option, Schema } from "effect" +import { ServiceMap, Effect, Layer, Option, Schema } from "effect" import { TreeFormatter } from "effect/ParseResult" import { createRemoteJWKSet, jwtVerify } from "jose" import { UserLookupCache } from "../cache/user-lookup-cache.ts" @@ -36,7 +36,6 @@ export class ProxyAuthenticationError extends Schema.TaggedErrorClass()("@hazel/auth/ProxyAuth", { - dependencies: [UserLookupCache.Default, WorkOSClient.Default], make: Effect.gen(function* () { const userLookupCache = yield* UserLookupCache const workos = yield* WorkOSClient @@ -205,7 +204,12 @@ export class ProxyAuth extends ServiceMap.Service()("@hazel/auth/Prox validateBearerToken, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(UserLookupCache.Default), + Layer.provide(WorkOSClient.Default), + ) +} /** * Layer that provides ProxyAuth with all its dependencies via Effect.Service dependencies. diff --git a/packages/auth/src/session/workos-client.ts b/packages/auth/src/session/workos-client.ts index eefc22008..6325e1784 100644 --- a/packages/auth/src/session/workos-client.ts +++ b/packages/auth/src/session/workos-client.ts @@ -11,7 +11,6 @@ import { AuthConfig } from "../config.ts" * Provides type-safe access to WorkOS SDK operations. */ export class WorkOSClient extends ServiceMap.Service()("@hazel/auth/WorkOSClient", { - dependencies: [AuthConfig.Default], make: Effect.gen(function* () { const config = yield* AuthConfig const client = new WorkOSNodeAPI(config.workosApiKey, { @@ -55,6 +54,10 @@ export class WorkOSClient extends ServiceMap.Service()("@hazel/aut } }), }) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(AuthConfig.Default), + ) + /** Default mock user for tests */ static readonly mockUser: WorkOSUser = { id: Schema.decodeUnknownSync(WorkOSUserId)("user_01ABC123"), diff --git a/packages/backend-core/src/repositories/attachment-repo.ts b/packages/backend-core/src/repositories/attachment-repo.ts index 3d93a2cf2..f2b4b2c78 100644 --- a/packages/backend-core/src/repositories/attachment-repo.ts +++ b/packages/backend-core/src/repositories/attachment-repo.ts @@ -11,4 +11,6 @@ export class AttachmentRepo extends ServiceMap.Service()("Attach return baseRepo }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/bot-command-repo.ts b/packages/backend-core/src/repositories/bot-command-repo.ts index e50eb9381..dc750c05d 100644 --- a/packages/backend-core/src/repositories/bot-command-repo.ts +++ b/packages/backend-core/src/repositories/bot-command-repo.ts @@ -158,4 +158,6 @@ export class BotCommandRepo extends ServiceMap.Service()("BotCom deleteStaleCommands, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/bot-installation-repo.ts b/packages/backend-core/src/repositories/bot-installation-repo.ts index 831da6d53..6c2227fdd 100644 --- a/packages/backend-core/src/repositories/bot-installation-repo.ts +++ b/packages/backend-core/src/repositories/bot-installation-repo.ts @@ -76,4 +76,6 @@ export class BotInstallationRepo extends ServiceMap.Service getBotIdsForOrg, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/bot-repo.ts b/packages/backend-core/src/repositories/bot-repo.ts index 7e70e4d85..6f76b8582 100644 --- a/packages/backend-core/src/repositories/bot-repo.ts +++ b/packages/backend-core/src/repositories/bot-repo.ts @@ -261,4 +261,6 @@ export class BotRepo extends ServiceMap.Service()("BotRepo", { decrementInstallCount, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/channel-member-repo.ts b/packages/backend-core/src/repositories/channel-member-repo.ts index d8e6130ea..d6898b653 100644 --- a/packages/backend-core/src/repositories/channel-member-repo.ts +++ b/packages/backend-core/src/repositories/channel-member-repo.ts @@ -102,4 +102,6 @@ export class ChannelMemberRepo extends ServiceMap.Service()(" listByChannel, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/channel-repo.ts b/packages/backend-core/src/repositories/channel-repo.ts index 962d5ae27..a4332419b 100644 --- a/packages/backend-core/src/repositories/channel-repo.ts +++ b/packages/backend-core/src/repositories/channel-repo.ts @@ -36,4 +36,6 @@ export class ChannelRepo extends ServiceMap.Service()("ChannelRepo" findByOrgAndName, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/channel-section-repo.ts b/packages/backend-core/src/repositories/channel-section-repo.ts index 271ce25d1..8ac5e128e 100644 --- a/packages/backend-core/src/repositories/channel-section-repo.ts +++ b/packages/backend-core/src/repositories/channel-section-repo.ts @@ -15,4 +15,6 @@ export class ChannelSectionRepo extends ServiceMap.Service() return baseRepo }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/channel-webhook-repo.ts b/packages/backend-core/src/repositories/channel-webhook-repo.ts index 654539153..a0856aa97 100644 --- a/packages/backend-core/src/repositories/channel-webhook-repo.ts +++ b/packages/backend-core/src/repositories/channel-webhook-repo.ts @@ -120,4 +120,6 @@ export class ChannelWebhookRepo extends ServiceMap.Service() findByOrganization, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/connect-invite-repo.ts b/packages/backend-core/src/repositories/connect-invite-repo.ts index a55d92912..4b13daee1 100644 --- a/packages/backend-core/src/repositories/connect-invite-repo.ts +++ b/packages/backend-core/src/repositories/connect-invite-repo.ts @@ -90,4 +90,6 @@ export class ConnectInviteRepo extends ServiceMap.Service()(" findPendingForGuestOrganization, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/custom-emoji-repo.ts b/packages/backend-core/src/repositories/custom-emoji-repo.ts index 4c334dd83..d7c07cede 100644 --- a/packages/backend-core/src/repositories/custom-emoji-repo.ts +++ b/packages/backend-core/src/repositories/custom-emoji-repo.ts @@ -98,4 +98,6 @@ export class CustomEmojiRepo extends ServiceMap.Service()("Cust restore, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/integration-token-repo.ts b/packages/backend-core/src/repositories/integration-token-repo.ts index 0f34d9b8e..04c824065 100644 --- a/packages/backend-core/src/repositories/integration-token-repo.ts +++ b/packages/backend-core/src/repositories/integration-token-repo.ts @@ -94,4 +94,6 @@ export class IntegrationTokenRepo extends ServiceMap.Service()("Invita bulkUpsertByWorkosId, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/message-outbox-repo.ts b/packages/backend-core/src/repositories/message-outbox-repo.ts index 37a23276d..bdb67963c 100644 --- a/packages/backend-core/src/repositories/message-outbox-repo.ts +++ b/packages/backend-core/src/repositories/message-outbox-repo.ts @@ -31,25 +31,25 @@ export const ReactionDeletedPayloadSchema = Schema.Struct({ userId: Schema.optional(UserId), }) -export const MessageOutboxEventType = Schema.Literal( +export const MessageOutboxEventType = Schema.Literals([ "message_created", "message_updated", "message_deleted", "reaction_created", "reaction_deleted", -) +]) export type MessageOutboxEventType = Schema.Schema.Type -export const MessageOutboxEventStatus = Schema.Literal("pending", "processing", "processed", "failed") +export const MessageOutboxEventStatus = Schema.Literals(["pending", "processing", "processed", "failed"]) export type MessageOutboxEventStatus = Schema.Schema.Type -export const MessageOutboxEventPayloadSchema = Schema.Union( +export const MessageOutboxEventPayloadSchema = Schema.Union([ MessageCreatedPayloadSchema, MessageUpdatedPayloadSchema, MessageDeletedPayloadSchema, ReactionCreatedPayloadSchema, ReactionDeletedPayloadSchema, -) +]) export type MessageOutboxEventPayload = Schema.Schema.Type export type MessageCreatedPayload = Schema.Schema.Type @@ -267,4 +267,6 @@ export class MessageOutboxRepo extends ServiceMap.Service()(" markFailed, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/message-reaction-repo.ts b/packages/backend-core/src/repositories/message-reaction-repo.ts index aeb1a1d99..8b2a3b36c 100644 --- a/packages/backend-core/src/repositories/message-reaction-repo.ts +++ b/packages/backend-core/src/repositories/message-reaction-repo.ts @@ -61,4 +61,6 @@ export class MessageReactionRepo extends ServiceMap.Service backfillConversationIdForChannel, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/message-repo.ts b/packages/backend-core/src/repositories/message-repo.ts index 18f7fca3b..6882c2a24 100644 --- a/packages/backend-core/src/repositories/message-repo.ts +++ b/packages/backend-core/src/repositories/message-repo.ts @@ -247,4 +247,6 @@ export class MessageRepo extends ServiceMap.Service()("MessageRepo" backfillConversationIdForChannel, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/notification-repo.ts b/packages/backend-core/src/repositories/notification-repo.ts index 598b6d343..58c8dafcd 100644 --- a/packages/backend-core/src/repositories/notification-repo.ts +++ b/packages/backend-core/src/repositories/notification-repo.ts @@ -77,4 +77,6 @@ export class NotificationRepo extends ServiceMap.Service()("No deleteByChannelId, } as const }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/organization-repo.ts b/packages/backend-core/src/repositories/organization-repo.ts index 62f3bf8ec..2a5e6dc11 100644 --- a/packages/backend-core/src/repositories/organization-repo.ts +++ b/packages/backend-core/src/repositories/organization-repo.ts @@ -1,7 +1,7 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" import type { OrganizationId, UserId } from "@hazel/schema" import { Organization } from "@hazel/domain/models" -import { ServiceMap, Effect, Option, type Schema } from "effect" +import { ServiceMap, Effect, Layer, Option, type Schema } from "effect" import { ChannelMemberRepo } from "./channel-member-repo" import { ChannelRepo } from "./channel-repo" @@ -116,5 +116,9 @@ export class OrganizationRepo extends ServiceMap.Service()("Or setupDefaultChannels, } }), - dependencies: [ChannelRepo.Default, ChannelMemberRepo.Default], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(ChannelRepo.Default), + Layer.provide(ChannelMemberRepo.Default), + ) +} diff --git a/packages/backend-core/src/repositories/pinned-message-repo.ts b/packages/backend-core/src/repositories/pinned-message-repo.ts index c2bad2fae..1bcd15530 100644 --- a/packages/backend-core/src/repositories/pinned-message-repo.ts +++ b/packages/backend-core/src/repositories/pinned-message-repo.ts @@ -15,4 +15,6 @@ export class PinnedMessageRepo extends ServiceMap.Service()(" return baseRepo }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/rss-subscription-repo.ts b/packages/backend-core/src/repositories/rss-subscription-repo.ts index 3ba8d3e51..b0e3c272a 100644 --- a/packages/backend-core/src/repositories/rss-subscription-repo.ts +++ b/packages/backend-core/src/repositories/rss-subscription-repo.ts @@ -125,4 +125,6 @@ export class RssSubscriptionRepo extends ServiceMap.Service softDelete, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/typing-indicator-repo.ts b/packages/backend-core/src/repositories/typing-indicator-repo.ts index df1df19ff..b4ba3ec50 100644 --- a/packages/backend-core/src/repositories/typing-indicator-repo.ts +++ b/packages/backend-core/src/repositories/typing-indicator-repo.ts @@ -90,4 +90,6 @@ export class TypingIndicatorRepo extends ServiceMap.Service deleteStale, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/repositories/user-repo.ts b/packages/backend-core/src/repositories/user-repo.ts index bf96ffae0..938d84f0f 100644 --- a/packages/backend-core/src/repositories/user-repo.ts +++ b/packages/backend-core/src/repositories/user-repo.ts @@ -118,4 +118,6 @@ export class UserRepo extends ServiceMap.Service()("UserRepo", { bulkUpsertByExternalId, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend-core/src/services/workos-sync.ts b/packages/backend-core/src/services/workos-sync.ts index e461bcf12..64547283d 100644 --- a/packages/backend-core/src/services/workos-sync.ts +++ b/packages/backend-core/src/services/workos-sync.ts @@ -8,7 +8,7 @@ import { type UserId, } from "@hazel/schema" import type { Event } from "@workos-inc/node" -import { ServiceMap, Effect, Match, Option, pipe, Schema, Stream } from "effect" +import { ServiceMap, Effect, Layer, Match, Option, pipe, Schema, Stream } from "effect" import { TreeFormatter } from "effect/ParseResult" import { InvitationRepo } from "../repositories/invitation-repo" import { OrganizationMemberRepo } from "../repositories/organization-member-repo" @@ -991,11 +991,12 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { processWebhookEvent: (event: Event) => processWebhookEvent(event), } }), - dependencies: [ - WorkOSClient.Default, - UserRepo.Default, - OrganizationRepo.Default, - OrganizationMemberRepo.Default, - InvitationRepo.Default, - ], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(WorkOSClient.Default), + Layer.provide(UserRepo.Default), + Layer.provide(OrganizationRepo.Default), + Layer.provide(OrganizationMemberRepo.Default), + Layer.provide(InvitationRepo.Default), + ) +} diff --git a/packages/backend-core/src/services/workos.ts b/packages/backend-core/src/services/workos.ts index bb932ff88..e63cf3a6e 100644 --- a/packages/backend-core/src/services/workos.ts +++ b/packages/backend-core/src/services/workos.ts @@ -24,4 +24,6 @@ export class WorkOSClient extends ServiceMap.Service()("WorkOSClie call, } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/db/src/services/drizzle-effect.ts b/packages/db/src/services/drizzle-effect.ts index 2ffd5af0e..765d7dc0f 100644 --- a/packages/db/src/services/drizzle-effect.ts +++ b/packages/db/src/services/drizzle-effect.ts @@ -56,26 +56,26 @@ type StrictJsonArray = readonly StrictJsonValue[] type StrictJsonValue = JsonPrimitive | StrictJsonObject | StrictJsonArray // Non-recursive JSON schema to avoid type inference explosion -export const JsonValue = Schema.Union( +export const JsonValue = Schema.Union([ Schema.String, Schema.Number, Schema.Boolean, Schema.Null, Schema.Record({ key: Schema.String, value: Schema.Unknown }), Schema.Array(Schema.Unknown), -) satisfies Schema.Schema +]) satisfies Schema.Schema // For cases where you need full JSON validation, use this explicit version export const StrictJsonValue = Schema.suspend( (): Schema.Schema => - Schema.Union( + Schema.Union([ Schema.String, Schema.Number, Schema.Boolean, Schema.Null, Schema.Record({ key: Schema.String, value: StrictJsonValue }), Schema.Array(StrictJsonValue), - ), + ]), ) // Utility type to prevent unknown keys (similar to drizzle-zod) @@ -255,7 +255,7 @@ function mapColumnToSchema(column: Drizzle.Column): Schema.Schema { let type: Schema.Schema | undefined if (isWithEnum(column)) { - type = column.enumValues.length > 0 ? Schema.Literal(...column.enumValues) : Schema.String + type = column.enumValues.length > 0 ? Schema.Literals(column.enumValues) : Schema.String } if (!type) { diff --git a/packages/domain/src/bot-gateway.ts b/packages/domain/src/bot-gateway.ts index 5e4df7b9c..e9a3f7a4c 100644 --- a/packages/domain/src/bot-gateway.ts +++ b/packages/domain/src/bot-gateway.ts @@ -76,7 +76,7 @@ export const BotGatewayChannelMemberRemoveEnvelope = Schema.Struct({ payload: ChannelMember.Model.json, }) -export const BotGatewayEnvelope = Schema.Union( +export const BotGatewayEnvelope = Schema.Union([ BotGatewayCommandInvokeEnvelope, BotGatewayMessageCreateEnvelope, BotGatewayMessageUpdateEnvelope, @@ -86,7 +86,7 @@ export const BotGatewayEnvelope = Schema.Union( BotGatewayChannelDeleteEnvelope, BotGatewayChannelMemberAddEnvelope, BotGatewayChannelMemberRemoveEnvelope, -) +]) export type BotGatewayEnvelope = Schema.Schema.Type export type BotGatewayEventType = BotGatewayEnvelope["eventType"] @@ -128,12 +128,12 @@ export const BotGatewayHeartbeatFrame = Schema.Struct({ sessionId: Schema.optional(Schema.String), }) -export const BotGatewayClientFrame = Schema.Union( +export const BotGatewayClientFrame = Schema.Union([ BotGatewayIdentifyFrame, BotGatewayResumeFrame, BotGatewayAckFrame, BotGatewayHeartbeatFrame, -) +]) export type BotGatewayClientFrame = Schema.Schema.Type @@ -171,13 +171,13 @@ export const BotGatewayInvalidSessionFrame = Schema.Struct({ reason: Schema.String, }) -export const BotGatewayServerFrame = Schema.Union( +export const BotGatewayServerFrame = Schema.Union([ BotGatewayHelloFrame, BotGatewayReadyFrame, BotGatewayDispatchFrame, BotGatewayHeartbeatAckFrame, BotGatewayReconnectFrame, BotGatewayInvalidSessionFrame, -) +]) export type BotGatewayServerFrame = Schema.Schema.Type diff --git a/packages/domain/src/cluster/activities/cleanup-activities.ts b/packages/domain/src/cluster/activities/cleanup-activities.ts index a5717b9f5..478645585 100644 --- a/packages/domain/src/cluster/activities/cleanup-activities.ts +++ b/packages/domain/src/cluster/activities/cleanup-activities.ts @@ -52,4 +52,4 @@ export class MarkUploadsFailedError extends Schema.TaggedErrorClass("CurrentUserSchema")({ settings: S.NullOr(User.UserSettingsSchema), }) {} -export class Context extends C.Tag("CurrentUser")() {} +export class Context extends ServiceMap.Service()("CurrentUser") {} -const AuthFailure = S.Union( +const AuthFailure = S.Union([ UnauthorizedError, SessionLoadError, SessionAuthenticationError, @@ -39,11 +39,12 @@ const AuthFailure = S.Union( SessionExpiredError, InvalidBearerTokenError, WorkOSUserFetchError, -) +]) -export class Authorization extends HttpApiMiddleware.Tag()("Authorization", { - failure: AuthFailure, - provides: Context, +export class Authorization extends HttpApiMiddleware.Service()("Authorization", { + error: AuthFailure, security: { bearer: HttpApiSecurity.bearer, }, diff --git a/packages/domain/src/desktop-auth-errors.ts b/packages/domain/src/desktop-auth-errors.ts index 2d9a37a32..4fb97dbd6 100644 --- a/packages/domain/src/desktop-auth-errors.ts +++ b/packages/domain/src/desktop-auth-errors.ts @@ -16,7 +16,7 @@ export class TauriNotAvailableError extends Schema.TaggedErrorClass()("TokenStoreError", { message: Schema.String, - operation: Schema.Literal("load", "get", "set", "delete"), + operation: Schema.Literals(["load", "get", "set", "delete"]), detail: Schema.optional(Schema.String), }) {} @@ -74,7 +74,7 @@ export class TokenStoreError extends Schema.TaggedErrorClass()( */ export class TokenNotFoundError extends Schema.TaggedErrorClass()("TokenNotFoundError", { message: Schema.String, - tokenType: Schema.Literal("access", "refresh", "expiresAt"), + tokenType: Schema.Literals(["access", "refresh", "expiresAt"]), }) {} // ============================================================================ diff --git a/packages/domain/src/errors.ts b/packages/domain/src/errors.ts index fa4009655..0a108e908 100644 --- a/packages/domain/src/errors.ts +++ b/packages/domain/src/errors.ts @@ -1,4 +1,3 @@ -import { HttpApiSchema } from "effect/unstable/httpapi" import { Effect, Predicate, Schema } from "effect" import { ChannelId, MessageId } from "@hazel/schema" @@ -8,7 +7,7 @@ export class UnauthorizedError extends Schema.TaggedErrorClass( @@ -49,7 +48,7 @@ export class WorkflowInitializationError extends Schema.TaggedErrorClass( @@ -60,7 +59,7 @@ export class DmChannelAlreadyExistsError extends Schema.TaggedErrorClass( diff --git a/packages/domain/src/http/api-v1/messages.ts b/packages/domain/src/http/api-v1/messages.ts index c1c12c8d6..11451f522 100644 --- a/packages/domain/src/http/api-v1/messages.ts +++ b/packages/domain/src/http/api-v1/messages.ts @@ -76,7 +76,7 @@ export class ChannelNotFoundError extends Schema.TaggedErrorClass()( @@ -84,7 +84,7 @@ export class InvalidPaginationError extends Schema.TaggedErrorClass // OpenStatus status type -export const OpenStatusStatus = Schema.Literal("degraded", "error", "recovered") +export const OpenStatusStatus = Schema.Literals(["degraded", "error", "recovered"]) export type OpenStatusStatus = Schema.Schema.Type // OpenStatus webhook payload @@ -105,7 +105,7 @@ export class WebhookNotFoundError extends Schema.TaggedErrorClass("Klipy { message: Schema.String, }, - HttpApiSchema.status(502), + { httpApiStatus: 502 }, ) {} // ============ API Group ============ diff --git a/packages/domain/src/http/mock-data.ts b/packages/domain/src/http/mock-data.ts index a16af43aa..a0acb6952 100644 --- a/packages/domain/src/http/mock-data.ts +++ b/packages/domain/src/http/mock-data.ts @@ -34,7 +34,7 @@ export class MockDataGroup extends HttpApiGroup.make("mockData") .addError(UnauthorizedError) .addError(InternalServerError) .annotateContext( - OpenApi.annotations({ + OpenApi.annotate({ title: "Generate Mock Data", description: "Generate mock data for an organization", summary: "Generate test data", diff --git a/packages/domain/src/http/presence.ts b/packages/domain/src/http/presence.ts index 75196f3c0..dd4692914 100644 --- a/packages/domain/src/http/presence.ts +++ b/packages/domain/src/http/presence.ts @@ -21,7 +21,7 @@ export class PresencePublicGroup extends HttpApiGroup.make("presencePublic") .addSuccess(MarkOfflineResponse) .addError(InternalServerError) .annotateContext( - OpenApi.annotations({ + OpenApi.annotate({ title: "Mark User Offline", description: "Mark a user as offline when they close their tab (no auth required)", summary: "Mark offline", diff --git a/packages/domain/src/http/uploads.ts b/packages/domain/src/http/uploads.ts index 1126607c5..e4b2b9996 100644 --- a/packages/domain/src/http/uploads.ts +++ b/packages/domain/src/http/uploads.ts @@ -16,13 +16,13 @@ export const ALLOWED_EMOJI_TYPES = ["image/png", "image/gif", "image/webp"] as c // ============ Upload Type Schema ============ -export const UploadType = Schema.Literal( +export const UploadType = Schema.Literals([ "user-avatar", "bot-avatar", "organization-avatar", "attachment", "custom-emoji", -) +]) export type UploadType = typeof UploadType.Type // ============ Request Schemas ============ @@ -133,13 +133,13 @@ export class CustomEmojiUploadRequest extends Schema.Class("UploadErr { message: Schema.String, }, - HttpApiSchema.status(500), + { httpApiStatus: 500 }, ) {} export class BotNotFoundForUploadError extends Schema.TaggedErrorClass( @@ -176,7 +176,7 @@ export class BotNotFoundForUploadError extends Schema.TaggedErrorClass( @@ -186,7 +186,7 @@ export class OrganizationNotFoundForUploadError extends Schema.TaggedErrorClass< { organizationId: OrganizationId, }, - HttpApiSchema.status(404), + { httpApiStatus: 404 }, ) {} // ============ API Group ============ diff --git a/packages/domain/src/http/webhooks.ts b/packages/domain/src/http/webhooks.ts index 9842f53b5..3b1d7da03 100644 --- a/packages/domain/src/http/webhooks.ts +++ b/packages/domain/src/http/webhooks.ts @@ -23,7 +23,7 @@ export class InvalidWebhookSignature extends Schema.TaggedErrorClass export class Model extends M.Class("Attachment")({ diff --git a/packages/domain/src/models/bot-command-model.ts b/packages/domain/src/models/bot-command-model.ts index 321a26c3a..fdd8dbfad 100644 --- a/packages/domain/src/models/bot-command-model.ts +++ b/packages/domain/src/models/bot-command-model.ts @@ -11,7 +11,7 @@ export const BotCommandArgument = Schema.Struct({ description: Schema.NullOr(Schema.String), required: Schema.Boolean, placeholder: Schema.NullOr(Schema.String), - type: Schema.Literal("string", "number", "user", "channel"), + type: Schema.Literals(["string", "number", "user", "channel"]), }) export type BotCommandArgument = typeof BotCommandArgument.Type diff --git a/packages/domain/src/models/channel-model.ts b/packages/domain/src/models/channel-model.ts index e1324f21d..6989455bb 100644 --- a/packages/domain/src/models/channel-model.ts +++ b/packages/domain/src/models/channel-model.ts @@ -3,7 +3,7 @@ import { Schema } from "effect" import * as M from "./utils" import { baseFields } from "./utils" -export const ChannelType = Schema.Literal("public", "private", "thread", "direct", "single") +export const ChannelType = Schema.Literals(["public", "private", "thread", "direct", "single"]) export type ChannelType = Schema.Schema.Type export class Model extends M.Class("Channel")({ diff --git a/packages/domain/src/models/chat-sync-channel-link-model.ts b/packages/domain/src/models/chat-sync-channel-link-model.ts index 31f1fb60c..3ad3bc284 100644 --- a/packages/domain/src/models/chat-sync-channel-link-model.ts +++ b/packages/domain/src/models/chat-sync-channel-link-model.ts @@ -3,10 +3,10 @@ import { Schema } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const ChatSyncDirection = Schema.Literal("both", "hazel_to_external", "external_to_hazel") +export const ChatSyncDirection = Schema.Literals(["both", "hazel_to_external", "external_to_hazel"]) export type ChatSyncDirection = Schema.Schema.Type -export const ChatSyncOutboundIdentityStrategy = Schema.Literal("webhook", "fallback_bot") +export const ChatSyncOutboundIdentityStrategy = Schema.Literals(["webhook", "fallback_bot"]) export type ChatSyncOutboundIdentityStrategy = Schema.Schema.Type export const DiscordWebhookOutboundIdentityConfig = Schema.Struct({ @@ -26,13 +26,13 @@ export const SlackWebhookOutboundIdentityConfig = Schema.Struct({ }) export type SlackWebhookOutboundIdentityConfig = Schema.Schema.Type -export const ProviderOutboundConfig = Schema.Union( +export const ProviderOutboundConfig = Schema.Union([ DiscordWebhookOutboundIdentityConfig, SlackWebhookOutboundIdentityConfig, Schema.Struct({ kind: Schema.NonEmptyTrimmedString, }), -) +]) export type ProviderOutboundConfig = Schema.Schema.Type export const OutboundIdentityProviders = Schema.Record({ diff --git a/packages/domain/src/models/chat-sync-connection-model.ts b/packages/domain/src/models/chat-sync-connection-model.ts index e9a06c5f0..7b48e887a 100644 --- a/packages/domain/src/models/chat-sync-connection-model.ts +++ b/packages/domain/src/models/chat-sync-connection-model.ts @@ -6,7 +6,7 @@ import { JsonDate } from "./utils" export const ChatSyncProvider = Schema.NonEmptyTrimmedString export type ChatSyncProvider = Schema.Schema.Type -export const ChatSyncConnectionStatus = Schema.Literal("active", "paused", "error", "disabled") +export const ChatSyncConnectionStatus = Schema.Literals(["active", "paused", "error", "disabled"]) export type ChatSyncConnectionStatus = Schema.Schema.Type export class Model extends M.Class("ChatSyncConnection")({ diff --git a/packages/domain/src/models/chat-sync-event-receipt-model.ts b/packages/domain/src/models/chat-sync-event-receipt-model.ts index db047bb7e..362e7b789 100644 --- a/packages/domain/src/models/chat-sync-event-receipt-model.ts +++ b/packages/domain/src/models/chat-sync-event-receipt-model.ts @@ -3,10 +3,10 @@ import { Schema } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const ChatSyncReceiptSource = Schema.Literal("hazel", "external") +export const ChatSyncReceiptSource = Schema.Literals(["hazel", "external"]) export type ChatSyncReceiptSource = Schema.Schema.Type -export const ChatSyncReceiptStatus = Schema.Literal("processed", "ignored", "failed") +export const ChatSyncReceiptStatus = Schema.Literals(["processed", "ignored", "failed"]) export type ChatSyncReceiptStatus = Schema.Schema.Type export class Model extends M.Class("ChatSyncEventReceipt")({ diff --git a/packages/domain/src/models/connect-conversation-channel-model.ts b/packages/domain/src/models/connect-conversation-channel-model.ts index d52d2e97e..b9aede904 100644 --- a/packages/domain/src/models/connect-conversation-channel-model.ts +++ b/packages/domain/src/models/connect-conversation-channel-model.ts @@ -3,7 +3,7 @@ import { Schema } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const ConnectConversationChannelRole = Schema.Literal("host", "guest") +export const ConnectConversationChannelRole = Schema.Literals(["host", "guest"]) export type ConnectConversationChannelRole = Schema.Schema.Type export class Model extends M.Class("ConnectConversationChannel")({ diff --git a/packages/domain/src/models/connect-conversation-model.ts b/packages/domain/src/models/connect-conversation-model.ts index 5cb83d393..0f0647f4f 100644 --- a/packages/domain/src/models/connect-conversation-model.ts +++ b/packages/domain/src/models/connect-conversation-model.ts @@ -3,7 +3,7 @@ import { Schema } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const ConnectConversationStatus = Schema.Literal("active", "disconnected") +export const ConnectConversationStatus = Schema.Literals(["active", "disconnected"]) export type ConnectConversationStatus = Schema.Schema.Type export class Model extends M.Class("ConnectConversation")({ diff --git a/packages/domain/src/models/connect-invite-model.ts b/packages/domain/src/models/connect-invite-model.ts index f83b70d0b..671096e4f 100644 --- a/packages/domain/src/models/connect-invite-model.ts +++ b/packages/domain/src/models/connect-invite-model.ts @@ -3,10 +3,10 @@ import { Schema } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const ConnectInviteStatus = Schema.Literal("pending", "accepted", "declined", "revoked", "expired") +export const ConnectInviteStatus = Schema.Literals(["pending", "accepted", "declined", "revoked", "expired"]) export type ConnectInviteStatus = Schema.Schema.Type -export const ConnectInviteTargetKind = Schema.Literal("slug", "email") +export const ConnectInviteTargetKind = Schema.Literals(["slug", "email"]) export type ConnectInviteTargetKind = Schema.Schema.Type export class Model extends M.Class("ConnectInvite")({ diff --git a/packages/domain/src/models/integration-connection-model.ts b/packages/domain/src/models/integration-connection-model.ts index 375f2b663..58c66f4c5 100644 --- a/packages/domain/src/models/integration-connection-model.ts +++ b/packages/domain/src/models/integration-connection-model.ts @@ -3,13 +3,13 @@ import { Schema } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const IntegrationProvider = Schema.Literal("linear", "github", "figma", "notion", "discord", "craft") +export const IntegrationProvider = Schema.Literals(["linear", "github", "figma", "notion", "discord", "craft"]) export type IntegrationProvider = Schema.Schema.Type -export const ConnectionLevel = Schema.Literal("organization", "user") +export const ConnectionLevel = Schema.Literals(["organization", "user"]) export type ConnectionLevel = Schema.Schema.Type -export const ConnectionStatus = Schema.Literal("active", "expired", "revoked", "error", "suspended") +export const ConnectionStatus = Schema.Literals(["active", "expired", "revoked", "error", "suspended"]) export type ConnectionStatus = Schema.Schema.Type export class Model extends M.Class("IntegrationConnection")({ diff --git a/packages/domain/src/models/integration-request-model.ts b/packages/domain/src/models/integration-request-model.ts index 1ddd519bd..72c8646af 100644 --- a/packages/domain/src/models/integration-request-model.ts +++ b/packages/domain/src/models/integration-request-model.ts @@ -3,7 +3,7 @@ import { Schema } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const IntegrationRequestStatus = Schema.Literal("pending", "reviewed", "planned", "rejected") +export const IntegrationRequestStatus = Schema.Literals(["pending", "reviewed", "planned", "rejected"]) export type IntegrationRequestStatus = Schema.Schema.Type export class Model extends M.Class("IntegrationRequest")({ diff --git a/packages/domain/src/models/invitation-model.ts b/packages/domain/src/models/invitation-model.ts index 75242befa..d6111a5c8 100644 --- a/packages/domain/src/models/invitation-model.ts +++ b/packages/domain/src/models/invitation-model.ts @@ -3,7 +3,7 @@ import { Schema } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const InvitationStatus = Schema.Literal("pending", "accepted", "expired", "revoked") +export const InvitationStatus = Schema.Literals(["pending", "accepted", "expired", "revoked"]) export type InvitationStatus = Schema.Schema.Type export class Model extends M.Class("Invitation")({ diff --git a/packages/domain/src/models/message-embed-schema.ts b/packages/domain/src/models/message-embed-schema.ts index 66e4d5d41..086712dd3 100644 --- a/packages/domain/src/models/message-embed-schema.ts +++ b/packages/domain/src/models/message-embed-schema.ts @@ -16,7 +16,7 @@ export const MessageEmbedFooter = Schema.Struct({ export type MessageEmbedFooter = Schema.Schema.Type // Badge intent for field styling -export const BadgeIntent = Schema.Literal( +export const BadgeIntent = Schema.Literals([ "primary", "secondary", "success", @@ -24,11 +24,11 @@ export const BadgeIntent = Schema.Literal( "warning", "danger", "outline", -) +]) export type BadgeIntent = Schema.Schema.Type // Field type for rendering mode -export const MessageEmbedFieldType = Schema.Literal("text", "badge") +export const MessageEmbedFieldType = Schema.Literals(["text", "badge"]) export type MessageEmbedFieldType = Schema.Schema.Type // Field options for type-specific settings @@ -57,8 +57,8 @@ export type MessageEmbedBadge = Schema.Schema.Type // Agent step for cached state (matches actor's AgentStep) export const CachedAgentStep = Schema.Struct({ id: Schema.String, - type: Schema.Literal("thinking", "tool_call", "tool_result", "text", "error"), - status: Schema.Literal("pending", "active", "completed", "failed"), + type: Schema.Literals(["thinking", "tool_call", "tool_result", "text", "error"]), + status: Schema.Literals(["pending", "active", "completed", "failed"]), content: Schema.optional(Schema.String), toolName: Schema.optional(Schema.String), toolInput: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), @@ -71,7 +71,7 @@ export type CachedAgentStep = Schema.Schema.Type // Live state cached snapshot for non-realtime clients export const MessageEmbedLiveStateCached = Schema.Struct({ - status: Schema.Literal("idle", "active", "completed", "failed"), + status: Schema.Literals(["idle", "active", "completed", "failed"]), data: Schema.Record({ key: Schema.String, value: Schema.Unknown }), text: Schema.optional(Schema.String), progress: Schema.optional(Schema.Number), @@ -85,7 +85,7 @@ export const MessageEmbedLoadingState = Schema.Struct({ /** Text to display while loading (default: "Thinking...") */ text: Schema.optional(Schema.String), /** Icon to display: "sparkle" or "brain" (default: "sparkle") */ - icon: Schema.optional(Schema.Literal("sparkle", "brain")), + icon: Schema.optional(Schema.Literals(["sparkle", "brain"])), /** Whether to show spinning animation on the icon (default: true) */ showSpinner: Schema.optional(Schema.Boolean), /** Whether to pulse/throb the entire loading indicator (default: false) */ diff --git a/packages/domain/src/models/message-integration-link-model.ts b/packages/domain/src/models/message-integration-link-model.ts index 2c145cfa7..137c33545 100644 --- a/packages/domain/src/models/message-integration-link-model.ts +++ b/packages/domain/src/models/message-integration-link-model.ts @@ -4,7 +4,7 @@ import { IntegrationProvider } from "./integration-connection-model" import * as M from "./utils" import { JsonDate } from "./utils" -export const LinkType = Schema.Literal("created", "mentioned", "resolved", "linked") +export const LinkType = Schema.Literals(["created", "mentioned", "resolved", "linked"]) export type LinkType = Schema.Schema.Type export class Model extends M.Class("MessageIntegrationLink")({ diff --git a/packages/domain/src/models/organization-member-model.ts b/packages/domain/src/models/organization-member-model.ts index 823f22a26..f84905594 100644 --- a/packages/domain/src/models/organization-member-model.ts +++ b/packages/domain/src/models/organization-member-model.ts @@ -3,7 +3,7 @@ import { Schema } from "effect" import * as M from "./utils" import { baseFields, JsonDate } from "./utils" -export const OrganizationRole = Schema.Literal("admin", "member", "owner") +export const OrganizationRole = Schema.Literals(["admin", "member", "owner"]) export type OrganizationRole = Schema.Schema.Type export class Model extends M.Class("OrganizationMember")({ diff --git a/packages/domain/src/models/theme-model.ts b/packages/domain/src/models/theme-model.ts index c0b7c4e6f..760f01adb 100644 --- a/packages/domain/src/models/theme-model.ts +++ b/packages/domain/src/models/theme-model.ts @@ -3,7 +3,7 @@ import { Schema } from "effect" /** * Available gray palette options */ -export const GrayPalette = Schema.Literal( +export const GrayPalette = Schema.Literals([ "gray", "gray-blue", "gray-cool", @@ -12,13 +12,13 @@ export const GrayPalette = Schema.Literal( "gray-iron", "gray-true", "gray-warm", -) +]) export type GrayPalette = Schema.Schema.Type /** * Border radius preset options */ -export const RadiusPreset = Schema.Literal("tight", "normal", "round", "full") +export const RadiusPreset = Schema.Literals(["tight", "normal", "round", "full"]) export type RadiusPreset = Schema.Schema.Type /** @@ -27,7 +27,7 @@ export type RadiusPreset = Schema.Schema.Type export const HexColor = Schema.String.pipe( Schema.pattern(/^#[0-9A-Fa-f]{6}$/), Schema.brand("HexColor"), - Schema.annotations({ message: () => "Must be a valid hex color (#RRGGBB)" }), + Schema.annotate({ message: () => "Must be a valid hex color (#RRGGBB)" }), ) export type HexColor = Schema.Schema.Type @@ -91,7 +91,7 @@ export type ThemePreset = Schema.Schema.Type /** * Display mode preference */ -export const DisplayMode = Schema.Literal("light", "dark", "system") +export const DisplayMode = Schema.Literals(["light", "dark", "system"]) export type DisplayMode = Schema.Schema.Type /** diff --git a/packages/domain/src/models/typing-indicator-model.ts b/packages/domain/src/models/typing-indicator-model.ts index 7a7092265..9db19c545 100644 --- a/packages/domain/src/models/typing-indicator-model.ts +++ b/packages/domain/src/models/typing-indicator-model.ts @@ -6,7 +6,7 @@ export class Model extends M.Class("TypingIndicator")({ id: M.Generated(TypingIndicatorId), channelId: ChannelId, memberId: ChannelMemberId, - lastTyped: Schema.Number.annotations({ + lastTyped: Schema.Number.annotate({ title: "LastTyped", description: "Unix timestamp of last typing activity", }), diff --git a/packages/domain/src/models/user-model.ts b/packages/domain/src/models/user-model.ts index c7bb0005d..3d4b92c29 100644 --- a/packages/domain/src/models/user-model.ts +++ b/packages/domain/src/models/user-model.ts @@ -4,7 +4,7 @@ import { UserThemeSettings } from "./theme-model" import * as M from "./utils" import { baseFields } from "./utils" -export const UserType = Schema.Literal("user", "machine") +export const UserType = Schema.Literals(["user", "machine"]) export type UserType = Schema.Schema.Type /** diff --git a/packages/domain/src/models/user-presence-status-model.ts b/packages/domain/src/models/user-presence-status-model.ts index fd6da59e7..c668f7e70 100644 --- a/packages/domain/src/models/user-presence-status-model.ts +++ b/packages/domain/src/models/user-presence-status-model.ts @@ -3,7 +3,7 @@ import { Schema } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const UserPresenceStatusEnum = Schema.Literal("online", "away", "busy", "dnd", "offline") +export const UserPresenceStatusEnum = Schema.Literals(["online", "away", "busy", "dnd", "offline"]) export type UserPresenceStatusEnum = Schema.Schema.Type export class Model extends M.Class("UserPresenceStatus")({ diff --git a/packages/domain/src/models/utils.ts b/packages/domain/src/models/utils.ts index 918887e60..7028d30b7 100644 --- a/packages/domain/src/models/utils.ts +++ b/packages/domain/src/models/utils.ts @@ -392,7 +392,7 @@ export const UuidV4Insert = ( }) /** A boolean parsed from 0 or 1. */ -export class BooleanFromNumber extends Schema.transform(Schema.Literal(0, 1), Schema.Boolean, { +export class BooleanFromNumber extends Schema.transform(Schema.Literals([0, 1]), Schema.Boolean, { decode: (n) => n === 1, encode: (b) => (b ? 1 : 0), }) {} @@ -407,8 +407,8 @@ export interface EntitySchema extends Schema.Schema.AnyNoContext { } // Helper utilities for common model fields -export const JsonDate = Schema.Union(Schema.DateFromString, Schema.DateFromSelf).pipe( - Schema.annotations({ +export const JsonDate = Schema.Union([Schema.DateFromString, Schema.DateFromSelf]).pipe( + Schema.annotate({ jsonSchema: { type: "string", format: "date-time" }, }), ) diff --git a/packages/domain/src/rate-limit-errors.ts b/packages/domain/src/rate-limit-errors.ts index 928813eeb..333b55adf 100644 --- a/packages/domain/src/rate-limit-errors.ts +++ b/packages/domain/src/rate-limit-errors.ts @@ -1,4 +1,3 @@ -import { HttpApiSchema } from "effect/unstable/httpapi" import { Schema } from "effect" /** @@ -13,5 +12,5 @@ export class RateLimitExceededError extends Schema.TaggedErrorClass("CreateDmChannelRequest")({ participantIds: Schema.Array(UserId), - type: Schema.Literal("direct", "single"), + type: Schema.Literals(["direct", "single"]), name: Schema.optional(Schema.String), organizationId: Schema.UUID, }) {} @@ -91,7 +91,7 @@ export class ChannelRpcs extends RpcGroup.make( Rpc.make("channel.create", { payload: CreateChannelRequest, success: ChannelResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["channels:write"]) .middleware(AuthMiddleware), @@ -113,7 +113,7 @@ export class ChannelRpcs extends RpcGroup.make( id: ChannelId, }).pipe(Schema.extend(Schema.partial(Channel.Model.jsonUpdate))), success: ChannelResponse, - error: Schema.Union(ChannelNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([ChannelNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["channels:write"]) .middleware(AuthMiddleware), @@ -133,7 +133,7 @@ export class ChannelRpcs extends RpcGroup.make( Rpc.make("channel.delete", { payload: Schema.Struct({ id: ChannelId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(ChannelNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([ChannelNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["channels:write"]) .middleware(AuthMiddleware), @@ -154,7 +154,7 @@ export class ChannelRpcs extends RpcGroup.make( Rpc.make("channel.createDm", { payload: CreateDmChannelRequest, success: ChannelResponse, - error: Schema.Union(DmChannelAlreadyExistsError, UnauthorizedError, InternalServerError), + error: Schema.Union([DmChannelAlreadyExistsError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["channels:write"]) .middleware(AuthMiddleware), @@ -176,7 +176,7 @@ export class ChannelRpcs extends RpcGroup.make( Rpc.make("channel.createThread", { payload: CreateThreadRequest, success: ChannelResponse, - error: Schema.Union(MessageNotFoundError, NestedThreadError, UnauthorizedError, InternalServerError), + error: Schema.Union([MessageNotFoundError, NestedThreadError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["channels:write"]) .middleware(AuthMiddleware), @@ -205,7 +205,7 @@ export class ChannelRpcs extends RpcGroup.make( Rpc.make("channel.generateName", { payload: Schema.Struct({ channelId: ChannelId }), success: Schema.Struct({ success: Schema.Boolean }), - error: Schema.Union( + error: Schema.Union([ ChannelNotFoundError, MessageNotFoundError, UnauthorizedError, @@ -219,7 +219,7 @@ export class ChannelRpcs extends RpcGroup.make( AIRateLimitError, AIResponseParseError, ThreadNameUpdateError, - ), + ]), }) .annotate(RequiredScopes, ["channels:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/chat-sync.ts b/packages/domain/src/rpc/chat-sync.ts index bf4326046..6c55744dd 100644 --- a/packages/domain/src/rpc/chat-sync.ts +++ b/packages/domain/src/rpc/chat-sync.ts @@ -92,12 +92,12 @@ export class ChatSyncRpcs extends RpcGroup.make( metadata: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), }), success: ChatSyncConnectionResponse, - error: Schema.Union( + error: Schema.Union([ ChatSyncConnectionExistsError, ChatSyncIntegrationNotConnectedError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["integration-connections:write"]) .middleware(AuthMiddleware), @@ -107,7 +107,7 @@ export class ChatSyncRpcs extends RpcGroup.make( organizationId: OrganizationId, }), success: ChatSyncConnectionListResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["integration-connections:read"]) .middleware(AuthMiddleware), @@ -119,7 +119,7 @@ export class ChatSyncRpcs extends RpcGroup.make( success: Schema.Struct({ transactionId: TransactionId, }), - error: Schema.Union(ChatSyncConnectionNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([ChatSyncConnectionNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["integration-connections:write"]) .middleware(AuthMiddleware), @@ -134,12 +134,12 @@ export class ChatSyncRpcs extends RpcGroup.make( settings: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), }), success: ChatSyncChannelLinkResponse, - error: Schema.Union( + error: Schema.Union([ ChatSyncConnectionNotFoundError, ChatSyncChannelLinkExistsError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["integration-connections:write"]) .middleware(AuthMiddleware), @@ -149,7 +149,7 @@ export class ChatSyncRpcs extends RpcGroup.make( syncConnectionId: SyncConnectionId, }), success: ChatSyncChannelLinkListResponse, - error: Schema.Union(ChatSyncConnectionNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([ChatSyncConnectionNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["integration-connections:read"]) .middleware(AuthMiddleware), @@ -161,7 +161,7 @@ export class ChatSyncRpcs extends RpcGroup.make( success: Schema.Struct({ transactionId: TransactionId, }), - error: Schema.Union(ChatSyncChannelLinkNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([ChatSyncChannelLinkNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["integration-connections:write"]) .middleware(AuthMiddleware), @@ -173,7 +173,7 @@ export class ChatSyncRpcs extends RpcGroup.make( isActive: Schema.optional(Schema.Boolean), }), success: ChatSyncChannelLinkResponse, - error: Schema.Union(ChatSyncChannelLinkNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([ChatSyncChannelLinkNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["integration-connections:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/connect-shares.ts b/packages/domain/src/rpc/connect-shares.ts index 89f45bb53..f4c81b244 100644 --- a/packages/domain/src/rpc/connect-shares.ts +++ b/packages/domain/src/rpc/connect-shares.ts @@ -93,7 +93,7 @@ export class ConnectShareRpcs extends RpcGroup.make( organizationId: OrganizationId, }), success: ConnectWorkspaceSearchResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["channels:read"]) .middleware(AuthMiddleware), @@ -109,12 +109,12 @@ export class ConnectShareRpcs extends RpcGroup.make( allowGuestMemberAdds: Schema.Boolean, }), success: ConnectInviteResponse, - error: Schema.Union( + error: Schema.Union([ ConnectWorkspaceNotFoundError, ConnectChannelAlreadySharedError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["channels:write"]) .middleware(AuthMiddleware), @@ -125,14 +125,14 @@ export class ConnectShareRpcs extends RpcGroup.make( guestOrganizationId: OrganizationId, }), success: ConnectConversationResponse, - error: Schema.Union( + error: Schema.Union([ ConnectInviteNotFoundError, ConnectInviteInvalidStateError, ConnectWorkspaceNotFoundError, ConnectChannelAlreadySharedError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), @@ -142,13 +142,13 @@ export class ConnectShareRpcs extends RpcGroup.make( inviteId: ConnectInviteId, }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union( + error: Schema.Union([ ConnectInviteNotFoundError, ConnectInviteInvalidStateError, ConnectWorkspaceNotFoundError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), @@ -158,12 +158,12 @@ export class ConnectShareRpcs extends RpcGroup.make( inviteId: ConnectInviteId, }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union( + error: Schema.Union([ ConnectInviteNotFoundError, ConnectInviteInvalidStateError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["channels:write"]) .middleware(AuthMiddleware), @@ -173,7 +173,7 @@ export class ConnectShareRpcs extends RpcGroup.make( organizationId: OrganizationId, }), success: ConnectInviteListResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organizations:read"]) .middleware(AuthMiddleware), @@ -183,7 +183,7 @@ export class ConnectShareRpcs extends RpcGroup.make( organizationId: OrganizationId, }), success: ConnectInviteListResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["channels:read"]) .middleware(AuthMiddleware), @@ -195,7 +195,7 @@ export class ConnectShareRpcs extends RpcGroup.make( status: Schema.optional(ConnectConversation.ConnectConversationStatus), }), success: ConnectConversationResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["channels:write"]) .middleware(AuthMiddleware), @@ -206,7 +206,7 @@ export class ConnectShareRpcs extends RpcGroup.make( userId: UserId, }), success: ConnectParticipantResponse, - error: Schema.Union(ConnectWorkspaceNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([ConnectWorkspaceNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["channel-members:write"]) .middleware(AuthMiddleware), @@ -217,7 +217,7 @@ export class ConnectShareRpcs extends RpcGroup.make( userId: UserId, }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(ConnectWorkspaceNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([ConnectWorkspaceNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["channel-members:write"]) .middleware(AuthMiddleware), @@ -228,7 +228,7 @@ export class ConnectShareRpcs extends RpcGroup.make( organizationId: OrganizationId, }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/custom-emojis.ts b/packages/domain/src/rpc/custom-emojis.ts index 8086e4bb4..bcb7b5663 100644 --- a/packages/domain/src/rpc/custom-emojis.ts +++ b/packages/domain/src/rpc/custom-emojis.ts @@ -70,12 +70,12 @@ export class CustomEmojiRpcs extends RpcGroup.make( imageUrl: Schema.String, }), success: CustomEmojiResponse, - error: Schema.Union( + error: Schema.Union([ CustomEmojiNameConflictError, CustomEmojiDeletedExistsError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["custom-emojis:write"]) .middleware(AuthMiddleware), @@ -98,12 +98,12 @@ export class CustomEmojiRpcs extends RpcGroup.make( name: Schema.optional(Schema.String), }), success: CustomEmojiResponse, - error: Schema.Union( + error: Schema.Union([ CustomEmojiNotFoundError, CustomEmojiNameConflictError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["custom-emojis:write"]) .middleware(AuthMiddleware), @@ -122,7 +122,7 @@ export class CustomEmojiRpcs extends RpcGroup.make( Rpc.make("customEmoji.delete", { payload: Schema.Struct({ id: CustomEmojiId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(CustomEmojiNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([CustomEmojiNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["custom-emojis:write"]) .middleware(AuthMiddleware), @@ -145,12 +145,12 @@ export class CustomEmojiRpcs extends RpcGroup.make( imageUrl: Schema.optional(Schema.String), }), success: CustomEmojiResponse, - error: Schema.Union( + error: Schema.Union([ CustomEmojiNotFoundError, CustomEmojiNameConflictError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["custom-emojis:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/github-subscriptions.ts b/packages/domain/src/rpc/github-subscriptions.ts index 39dde82c3..47bb6dac4 100644 --- a/packages/domain/src/rpc/github-subscriptions.ts +++ b/packages/domain/src/rpc/github-subscriptions.ts @@ -93,13 +93,13 @@ export class GitHubSubscriptionRpcs extends RpcGroup.make( branchFilter: Schema.optional(Schema.NullOr(Schema.String)), }), success: GitHubSubscriptionResponse, - error: Schema.Union( + error: Schema.Union([ ChannelNotFoundError, GitHubNotConnectedError, GitHubSubscriptionExistsError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["github-subscriptions:write"]) .middleware(AuthMiddleware), @@ -117,7 +117,7 @@ export class GitHubSubscriptionRpcs extends RpcGroup.make( Rpc.make("githubSubscription.list", { payload: Schema.Struct({ channelId: ChannelId }), success: GitHubSubscriptionListResponse, - error: Schema.Union(ChannelNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([ChannelNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["github-subscriptions:read"]) .middleware(AuthMiddleware), @@ -134,7 +134,7 @@ export class GitHubSubscriptionRpcs extends RpcGroup.make( Rpc.make("githubSubscription.listByOrganization", { payload: Schema.Struct({}), success: GitHubSubscriptionListResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["github-subscriptions:read"]) .middleware(AuthMiddleware), @@ -157,7 +157,7 @@ export class GitHubSubscriptionRpcs extends RpcGroup.make( isEnabled: Schema.optional(Schema.Boolean), }), success: GitHubSubscriptionResponse, - error: Schema.Union(GitHubSubscriptionNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([GitHubSubscriptionNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["github-subscriptions:write"]) .middleware(AuthMiddleware), @@ -175,7 +175,7 @@ export class GitHubSubscriptionRpcs extends RpcGroup.make( Rpc.make("githubSubscription.delete", { payload: Schema.Struct({ id: GitHubSubscriptionId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(GitHubSubscriptionNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([GitHubSubscriptionNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["github-subscriptions:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/integration-requests.ts b/packages/domain/src/rpc/integration-requests.ts index 3a0c24b3a..cef61cead 100644 --- a/packages/domain/src/rpc/integration-requests.ts +++ b/packages/domain/src/rpc/integration-requests.ts @@ -38,7 +38,7 @@ export class IntegrationRequestRpcs extends RpcGroup.make( Rpc.make("integrationRequest.create", { payload: CreateIntegrationRequestPayload, success: IntegrationRequestResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["integration-connections:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/invitations.ts b/packages/domain/src/rpc/invitations.ts index a03e74ae2..c1e6da8cd 100644 --- a/packages/domain/src/rpc/invitations.ts +++ b/packages/domain/src/rpc/invitations.ts @@ -69,12 +69,12 @@ export class InvitationRpcs extends RpcGroup.make( invites: Schema.Array( Schema.Struct({ email: Schema.String, - role: Schema.Literal("member", "admin"), + role: Schema.Literals(["member", "admin"]), }), ), }), success: InvitationBatchResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["invitations:write"]) .middleware(AuthMiddleware), @@ -94,7 +94,7 @@ export class InvitationRpcs extends RpcGroup.make( Rpc.make("invitation.resend", { payload: Schema.Struct({ invitationId: InvitationId }), success: InvitationResponse, - error: Schema.Union(InvitationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([InvitationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["invitations:write"]) .middleware(AuthMiddleware), @@ -114,7 +114,7 @@ export class InvitationRpcs extends RpcGroup.make( Rpc.make("invitation.revoke", { payload: Schema.Struct({ invitationId: InvitationId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(InvitationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([InvitationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["invitations:write"]) .middleware(AuthMiddleware), @@ -137,7 +137,7 @@ export class InvitationRpcs extends RpcGroup.make( ...Invitation.Model.jsonUpdate.fields, }), success: InvitationResponse, - error: Schema.Union(InvitationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([InvitationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["invitations:write"]) .middleware(AuthMiddleware), @@ -157,7 +157,7 @@ export class InvitationRpcs extends RpcGroup.make( Rpc.make("invitation.delete", { payload: Schema.Struct({ id: InvitationId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(InvitationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([InvitationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["invitations:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/message-reactions.ts b/packages/domain/src/rpc/message-reactions.ts index 560bacf0e..451cbe742 100644 --- a/packages/domain/src/rpc/message-reactions.ts +++ b/packages/domain/src/rpc/message-reactions.ts @@ -55,7 +55,7 @@ export class MessageReactionRpcs extends RpcGroup.make( data: Schema.optional(MessageReaction.Model.json), transactionId: TransactionId, }), - error: Schema.Union(MessageNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([MessageNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["message-reactions:write"]) .middleware(AuthMiddleware), @@ -75,7 +75,7 @@ export class MessageReactionRpcs extends RpcGroup.make( Rpc.make("messageReaction.create", { payload: MessageReaction.Insert, success: MessageReactionResponse, - error: Schema.Union(MessageNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([MessageNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["message-reactions:write"]) .middleware(AuthMiddleware), @@ -98,7 +98,7 @@ export class MessageReactionRpcs extends RpcGroup.make( ...MessageReaction.Model.jsonUpdate.fields, }), success: MessageReactionResponse, - error: Schema.Union(MessageReactionNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([MessageReactionNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["message-reactions:write"]) .middleware(AuthMiddleware), @@ -118,7 +118,7 @@ export class MessageReactionRpcs extends RpcGroup.make( Rpc.make("messageReaction.delete", { payload: Schema.Struct({ id: MessageReactionId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(MessageReactionNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([MessageReactionNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["message-reactions:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/messages.ts b/packages/domain/src/rpc/messages.ts index 27c82ffec..6c55063d3 100644 --- a/packages/domain/src/rpc/messages.ts +++ b/packages/domain/src/rpc/messages.ts @@ -71,12 +71,12 @@ export class MessageRpcs extends RpcGroup.make( Rpc.make("message.create", { payload: Message.Insert, success: MessageResponse, - error: Schema.Union( + error: Schema.Union([ ChannelNotFoundError, UnauthorizedError, InternalServerError, RateLimitExceededError, - ), + ]), }) .annotate(RequiredScopes, ["messages:write"]) .middleware(AuthMiddleware), @@ -99,12 +99,12 @@ export class MessageRpcs extends RpcGroup.make( id: MessageId, }).pipe(Schema.extend(Message.JsonUpdate)), success: MessageResponse, - error: Schema.Union( + error: Schema.Union([ MessageNotFoundError, UnauthorizedError, InternalServerError, RateLimitExceededError, - ), + ]), }) .annotate(RequiredScopes, ["messages:write"]) .middleware(AuthMiddleware), @@ -125,12 +125,12 @@ export class MessageRpcs extends RpcGroup.make( Rpc.make("message.delete", { payload: Schema.Struct({ id: MessageId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union( + error: Schema.Union([ MessageNotFoundError, UnauthorizedError, InternalServerError, RateLimitExceededError, - ), + ]), }) .annotate(RequiredScopes, ["messages:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/middleware.ts b/packages/domain/src/rpc/middleware.ts index 09986d07d..2699f42a3 100644 --- a/packages/domain/src/rpc/middleware.ts +++ b/packages/domain/src/rpc/middleware.ts @@ -42,7 +42,7 @@ import { * }) * ``` */ -const AuthFailure = S.Union( +const AuthFailure = S.Union([ UnauthorizedError, SessionLoadError, SessionAuthenticationError, @@ -52,10 +52,11 @@ const AuthFailure = S.Union( SessionExpiredError, InvalidBearerTokenError, WorkOSUserFetchError, -) +]) -export class AuthMiddleware extends RpcMiddleware.Tag()("AuthMiddleware", { - provides: CurrentUser.Context, - failure: AuthFailure, +export class AuthMiddleware extends RpcMiddleware.Service()("AuthMiddleware", { + error: AuthFailure, requiredForClient: true, }) {} diff --git a/packages/domain/src/rpc/notifications.ts b/packages/domain/src/rpc/notifications.ts index ae87022bb..05b39f0d6 100644 --- a/packages/domain/src/rpc/notifications.ts +++ b/packages/domain/src/rpc/notifications.ts @@ -42,7 +42,7 @@ export class NotificationRpcs extends RpcGroup.make( Rpc.make("notification.create", { payload: Notification.Model.jsonCreate, success: NotificationResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["notifications:write"]) .middleware(AuthMiddleware), @@ -65,7 +65,7 @@ export class NotificationRpcs extends RpcGroup.make( ...Notification.Model.jsonUpdate.fields, }), success: NotificationResponse, - error: Schema.Union(NotificationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([NotificationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["notifications:write"]) .middleware(AuthMiddleware), @@ -85,7 +85,7 @@ export class NotificationRpcs extends RpcGroup.make( Rpc.make("notification.delete", { payload: Schema.Struct({ id: NotificationId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(NotificationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([NotificationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["notifications:write"]) .middleware(AuthMiddleware), @@ -110,7 +110,7 @@ export class NotificationRpcs extends RpcGroup.make( deletedCount: Schema.Number, transactionId: TransactionId, }), - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["notifications:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/organization-members.ts b/packages/domain/src/rpc/organization-members.ts index 10821a717..4ba390e1b 100644 --- a/packages/domain/src/rpc/organization-members.ts +++ b/packages/domain/src/rpc/organization-members.ts @@ -77,7 +77,7 @@ export class OrganizationMemberRpcs extends RpcGroup.make( Rpc.make("organizationMember.create", { payload: OrganizationMember.Model.jsonCreate, success: OrganizationMemberResponse, - error: Schema.Union(OrganizationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([OrganizationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organization-members:write"]) .middleware(AuthMiddleware), @@ -100,7 +100,7 @@ export class OrganizationMemberRpcs extends RpcGroup.make( ...OrganizationMember.Model.jsonUpdate.fields, }), success: OrganizationMemberResponse, - error: Schema.Union(OrganizationMemberNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([OrganizationMemberNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organization-members:write"]) .middleware(AuthMiddleware), @@ -126,7 +126,7 @@ export class OrganizationMemberRpcs extends RpcGroup.make( }), }), success: OrganizationMemberResponse, - error: Schema.Union(OrganizationMemberNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([OrganizationMemberNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organization-members:write"]) .middleware(AuthMiddleware), @@ -146,7 +146,7 @@ export class OrganizationMemberRpcs extends RpcGroup.make( Rpc.make("organizationMember.delete", { payload: Schema.Struct({ id: OrganizationMemberId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(OrganizationMemberNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([OrganizationMemberNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organization-members:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/organizations.ts b/packages/domain/src/rpc/organizations.ts index f834c499d..bc2a6a697 100644 --- a/packages/domain/src/rpc/organizations.ts +++ b/packages/domain/src/rpc/organizations.ts @@ -72,7 +72,7 @@ export class OrganizationRpcs extends RpcGroup.make( Rpc.make("organization.create", { payload: Organization.Model.jsonCreate, success: OrganizationResponse, - error: Schema.Union(OrganizationSlugAlreadyExistsError, UnauthorizedError, InternalServerError), + error: Schema.Union([OrganizationSlugAlreadyExistsError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), @@ -82,12 +82,12 @@ export class OrganizationRpcs extends RpcGroup.make( id: OrganizationId, }).pipe(Schema.extend(Schema.partial(Organization.Model.jsonUpdate))), success: OrganizationResponse, - error: Schema.Union( + error: Schema.Union([ OrganizationNotFoundError, OrganizationSlugAlreadyExistsError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), @@ -95,7 +95,7 @@ export class OrganizationRpcs extends RpcGroup.make( Rpc.make("organization.delete", { payload: Schema.Struct({ id: OrganizationId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(OrganizationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([OrganizationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), @@ -106,12 +106,12 @@ export class OrganizationRpcs extends RpcGroup.make( slug: Schema.String, }), success: OrganizationResponse, - error: Schema.Union( + error: Schema.Union([ OrganizationNotFoundError, OrganizationSlugAlreadyExistsError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), @@ -127,7 +127,7 @@ export class OrganizationRpcs extends RpcGroup.make( isPublic: Schema.Boolean, }), success: OrganizationResponse, - error: Schema.Union(OrganizationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([OrganizationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), @@ -154,13 +154,13 @@ export class OrganizationRpcs extends RpcGroup.make( slug: Schema.String, }), success: OrganizationResponse, - error: Schema.Union( + error: Schema.Union([ OrganizationNotFoundError, PublicInviteDisabledError, AlreadyMemberError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), @@ -173,10 +173,10 @@ export class OrganizationRpcs extends RpcGroup.make( Rpc.make("organization.getAdminPortalLink", { payload: Schema.Struct({ id: OrganizationId, - intent: Schema.Literal("sso", "domain_verification", "dsync", "audit_logs", "log_streams"), + intent: Schema.Literals(["sso", "domain_verification", "dsync", "audit_logs", "log_streams"]), }), success: Schema.Struct({ link: Schema.String }), - error: Schema.Union(OrganizationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([OrganizationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), @@ -191,11 +191,11 @@ export class OrganizationRpcs extends RpcGroup.make( Schema.Struct({ id: Schema.String, domain: Schema.String, - state: Schema.Literal("pending", "verified", "failed", "legacy_verified"), + state: Schema.Literals(["pending", "verified", "failed", "legacy_verified"]), verificationToken: Schema.NullOr(Schema.String), }), ), - error: Schema.Union(OrganizationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([OrganizationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), @@ -215,7 +215,7 @@ export class OrganizationRpcs extends RpcGroup.make( state: Schema.String, verificationToken: Schema.NullOr(Schema.String), }), - error: Schema.Union(OrganizationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([OrganizationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), @@ -230,7 +230,7 @@ export class OrganizationRpcs extends RpcGroup.make( domainId: Schema.String, }), success: Schema.Struct({ success: Schema.Boolean }), - error: Schema.Union(OrganizationNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([OrganizationNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["organizations:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/pinned-messages.ts b/packages/domain/src/rpc/pinned-messages.ts index 75b98b121..3237c9d7b 100644 --- a/packages/domain/src/rpc/pinned-messages.ts +++ b/packages/domain/src/rpc/pinned-messages.ts @@ -77,7 +77,7 @@ export class PinnedMessageRpcs extends RpcGroup.make( messageId: MessageId, }), success: PinnedMessageResponse, - error: Schema.Union(MessageNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([MessageNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["pinned-messages:write"]) .middleware(AuthMiddleware), @@ -100,7 +100,7 @@ export class PinnedMessageRpcs extends RpcGroup.make( ...PinnedMessage.Model.jsonUpdate.fields, }), success: PinnedMessageResponse, - error: Schema.Union(PinnedMessageNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([PinnedMessageNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["pinned-messages:write"]) .middleware(AuthMiddleware), @@ -120,7 +120,7 @@ export class PinnedMessageRpcs extends RpcGroup.make( Rpc.make("pinnedMessage.delete", { payload: Schema.Struct({ id: PinnedMessageId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(PinnedMessageNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([PinnedMessageNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["pinned-messages:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/rss-subscriptions.ts b/packages/domain/src/rpc/rss-subscriptions.ts index c6db1b7cf..3a70991cc 100644 --- a/packages/domain/src/rpc/rss-subscriptions.ts +++ b/packages/domain/src/rpc/rss-subscriptions.ts @@ -52,13 +52,13 @@ export class RssSubscriptionRpcs extends RpcGroup.make( pollingIntervalMinutes: Schema.optional(Schema.Number), }), success: RssSubscriptionResponse, - error: Schema.Union( + error: Schema.Union([ ChannelNotFoundError, RssSubscriptionExistsError, RssFeedValidationError, UnauthorizedError, InternalServerError, - ), + ]), }) .annotate(RequiredScopes, ["rss-subscriptions:write"]) .middleware(AuthMiddleware), @@ -66,7 +66,7 @@ export class RssSubscriptionRpcs extends RpcGroup.make( Rpc.make("rssSubscription.list", { payload: Schema.Struct({ channelId: ChannelId }), success: RssSubscriptionListResponse, - error: Schema.Union(ChannelNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([ChannelNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["rss-subscriptions:read"]) .middleware(AuthMiddleware), @@ -74,7 +74,7 @@ export class RssSubscriptionRpcs extends RpcGroup.make( Rpc.make("rssSubscription.listByOrganization", { payload: Schema.Struct({}), success: RssSubscriptionListResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["rss-subscriptions:read"]) .middleware(AuthMiddleware), @@ -86,7 +86,7 @@ export class RssSubscriptionRpcs extends RpcGroup.make( pollingIntervalMinutes: Schema.optional(Schema.Number), }), success: RssSubscriptionResponse, - error: Schema.Union(RssSubscriptionNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([RssSubscriptionNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["rss-subscriptions:write"]) .middleware(AuthMiddleware), @@ -94,7 +94,7 @@ export class RssSubscriptionRpcs extends RpcGroup.make( Rpc.make("rssSubscription.delete", { payload: Schema.Struct({ id: RssSubscriptionId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(RssSubscriptionNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([RssSubscriptionNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["rss-subscriptions:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/scope-injection-middleware.ts b/packages/domain/src/rpc/scope-injection-middleware.ts index bf0f6a586..d6c9a2fa5 100644 --- a/packages/domain/src/rpc/scope-injection-middleware.ts +++ b/packages/domain/src/rpc/scope-injection-middleware.ts @@ -7,7 +7,7 @@ import { RpcMiddleware } from "effect/unstable/rpc" * Uses `wrap: true` so it wraps the handler with Effect.locally * to set the FiberRef value. */ -export class ScopeInjectionMiddleware extends RpcMiddleware.Tag()( +export class ScopeInjectionMiddleware extends RpcMiddleware.Service()( "ScopeInjectionMiddleware", { wrap: true }, ) {} diff --git a/packages/domain/src/rpc/typing-indicators.ts b/packages/domain/src/rpc/typing-indicators.ts index 79bf908cf..f5ee85bf4 100644 --- a/packages/domain/src/rpc/typing-indicators.ts +++ b/packages/domain/src/rpc/typing-indicators.ts @@ -88,7 +88,7 @@ export class TypingIndicatorRpcs extends RpcGroup.make( Rpc.make("typingIndicator.create", { payload: CreateTypingIndicatorPayload, success: TypingIndicatorResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["typing-indicators:write"]) .middleware(AuthMiddleware), @@ -111,7 +111,7 @@ export class TypingIndicatorRpcs extends RpcGroup.make( lastTyped: Schema.optional(Schema.Number), }), success: TypingIndicatorResponse, - error: Schema.Union(TypingIndicatorNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([TypingIndicatorNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["typing-indicators:write"]) .middleware(AuthMiddleware), @@ -131,7 +131,7 @@ export class TypingIndicatorRpcs extends RpcGroup.make( Rpc.make("typingIndicator.delete", { payload: Schema.Struct({ id: TypingIndicatorId }), success: TypingIndicatorResponse, - error: Schema.Union(TypingIndicatorNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([TypingIndicatorNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["typing-indicators:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/user-presence-status.ts b/packages/domain/src/rpc/user-presence-status.ts index e4181930c..bad80691b 100644 --- a/packages/domain/src/rpc/user-presence-status.ts +++ b/packages/domain/src/rpc/user-presence-status.ts @@ -73,7 +73,7 @@ export class UserPresenceStatusRpcs extends RpcGroup.make( suppressNotifications: Schema.optional(Schema.Boolean), }), success: UserPresenceStatusResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["user-presence-status:write"]) .middleware(AuthMiddleware), @@ -94,7 +94,7 @@ export class UserPresenceStatusRpcs extends RpcGroup.make( success: Schema.Struct({ lastSeenAt: JsonDate, }), - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["user-presence-status:write"]) .middleware(AuthMiddleware), @@ -112,7 +112,7 @@ export class UserPresenceStatusRpcs extends RpcGroup.make( Rpc.make("userPresenceStatus.clearStatus", { payload: Schema.Struct({}), success: UserPresenceStatusResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["user-presence-status:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/users.ts b/packages/domain/src/rpc/users.ts index 5fd654ab5..9fc7e9df3 100644 --- a/packages/domain/src/rpc/users.ts +++ b/packages/domain/src/rpc/users.ts @@ -38,7 +38,7 @@ export class UserRpcs extends RpcGroup.make( Rpc.make("user.me", { payload: Schema.Void, success: CurrentUser.Schema, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["users:read"]) .middleware(AuthMiddleware), @@ -60,7 +60,7 @@ export class UserRpcs extends RpcGroup.make( id: UserId, }).pipe(Schema.extend(Schema.partial(User.Model.jsonUpdate))), success: UserResponse, - error: Schema.Union(UserNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([UserNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["users:write"]) .middleware(AuthMiddleware), @@ -80,7 +80,7 @@ export class UserRpcs extends RpcGroup.make( Rpc.make("user.delete", { payload: Schema.Struct({ id: UserId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union(UserNotFoundError, UnauthorizedError, InternalServerError), + error: Schema.Union([UserNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["users:write"]) .middleware(AuthMiddleware), @@ -98,7 +98,7 @@ export class UserRpcs extends RpcGroup.make( Rpc.make("user.finalizeOnboarding", { payload: Schema.Void, success: UserResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["users:write"]) .middleware(AuthMiddleware), @@ -117,7 +117,7 @@ export class UserRpcs extends RpcGroup.make( Rpc.make("user.resetAvatar", { payload: Schema.Void, success: UserResponse, - error: Schema.Union(UnauthorizedError, InternalServerError), + error: Schema.Union([UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["users:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/scopes/api-scope.ts b/packages/domain/src/scopes/api-scope.ts index 8e181e740..e64f0d8ae 100644 --- a/packages/domain/src/scopes/api-scope.ts +++ b/packages/domain/src/scopes/api-scope.ts @@ -1,6 +1,6 @@ import { Schema } from "effect" -export const ApiScope = Schema.Literal( +export const ApiScope = Schema.Literals([ "organizations:read", "organizations:write", "channels:read", @@ -41,6 +41,6 @@ export const ApiScope = Schema.Literal( "user-presence-status:write", "users:read", "users:write", -) +]) export type ApiScope = typeof ApiScope.Type diff --git a/packages/domain/src/scopes/permission-error.ts b/packages/domain/src/scopes/permission-error.ts index d78d6e4a7..b39ea92e3 100644 --- a/packages/domain/src/scopes/permission-error.ts +++ b/packages/domain/src/scopes/permission-error.ts @@ -1,4 +1,3 @@ -import { HttpApiSchema } from "effect/unstable/httpapi" import { Predicate, Schema } from "effect" import type { ApiScope } from "./api-scope" @@ -8,7 +7,7 @@ export class PermissionError extends Schema.TaggedErrorClass("P message: Schema.String, requiredScope: Schema.optional(Schema.String), }, - HttpApiSchema.status(403), + { httpApiStatus: 403 }, ) { static is(u: unknown): u is PermissionError { return Predicate.isTagged(u, "PermissionError") diff --git a/packages/domain/src/session-errors.ts b/packages/domain/src/session-errors.ts index ef3b4853f..4c8d72530 100644 --- a/packages/domain/src/session-errors.ts +++ b/packages/domain/src/session-errors.ts @@ -1,4 +1,3 @@ -import { HttpApiSchema } from "effect/unstable/httpapi" import { Schema } from "effect" // 401 Errors - Client needs to re-authenticate @@ -10,7 +9,7 @@ export class SessionNotProvidedError extends Schema.TaggedErrorClass( @@ -21,7 +20,7 @@ export class SessionAuthenticationError extends Schema.TaggedErrorClass( @@ -32,7 +31,7 @@ export class InvalidJwtPayloadError extends Schema.TaggedErrorClass("SessionExpiredError")( @@ -41,7 +40,7 @@ export class SessionExpiredError extends Schema.TaggedErrorClass( @@ -52,7 +51,7 @@ export class InvalidBearerTokenError extends Schema.TaggedErrorClass( message: Schema.String, detail: Schema.String, }, - HttpApiSchema.status(503), + { httpApiStatus: 503 }, ) {} export class SessionRefreshError extends Schema.TaggedErrorClass("SessionRefreshError")( @@ -71,7 +70,7 @@ export class SessionRefreshError extends Schema.TaggedErrorClass("WorkOSUserFetchError")( @@ -80,5 +79,5 @@ export class WorkOSUserFetchError extends Schema.TaggedErrorClass url.replace(/\/\/.*@/, "//***@ * }).pipe(Effect.provide(Redis.Default)) * ``` */ -export class Redis extends Context.Tag("@hazel/effect-bun/Redis")< - Redis, +export class Redis extends ServiceMap.Service() { +>()("@hazel/effect-bun/Redis") { /** * Create a Redis layer with a specific URL */ @@ -296,7 +295,7 @@ export class Redis extends Context.Tag("@hazel/effect-bun/Redis")< /** * Create the Redis service implementation from a connected client */ -const makeService = (client: RedisClient, url: string): Context.Tag.Service => ({ +const makeService = (client: RedisClient, url: string): ServiceMap.Service.Shape => ({ // String operations get: (key) => Effect.tryPromise({ diff --git a/packages/effect-bun/src/S3.ts b/packages/effect-bun/src/S3.ts index 8fcfcda16..f0ebd9a9f 100644 --- a/packages/effect-bun/src/S3.ts +++ b/packages/effect-bun/src/S3.ts @@ -1,6 +1,6 @@ import type { BunFile, S3File, S3FilePresignOptions } from "bun" import { s3 as bunS3 } from "bun" -import { Context, Effect, Layer, Match, Schema } from "effect" +import { Effect, Layer, Match, Schema, ServiceMap } from "effect" // ============ Error Types ============ @@ -138,8 +138,7 @@ export type S3WriteData = string | ArrayBuffer | Uint8Array | Blob | Response | * }) * ``` */ -export class S3 extends Context.Tag("@hazel/effect-bun/S3")< - S3, +export class S3 extends ServiceMap.Service Effect.Effect } ->() { +>()("@hazel/effect-bun/S3") { static readonly Default = Layer.sync(S3, () => ({ file: (key) => Effect.try({ diff --git a/packages/integrations/src/craft/api-client.ts b/packages/integrations/src/craft/api-client.ts index bbbf25713..ffbd8793d 100644 --- a/packages/integrations/src/craft/api-client.ts +++ b/packages/integrations/src/craft/api-client.ts @@ -9,7 +9,7 @@ */ import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http" -import { ServiceMap, Duration, Effect, Schedule, Schema } from "effect" +import { ServiceMap, Duration, Effect, Layer, Schedule, Schema } from "effect" // ============================================================================ // Configuration @@ -21,7 +21,7 @@ const DEFAULT_TIMEOUT = Duration.seconds(30) // Domain Schemas (exported for consumers) // ============================================================================ -export const CraftBlockType = Schema.Literal( +export const CraftBlockType = Schema.Literals([ "text", "line", "page", @@ -42,7 +42,7 @@ export const CraftBlockType = Schema.Literal( "urlBlock", "videoBlock", "cardBlock", -) +]) export type CraftBlockType = typeof CraftBlockType.Type export const CraftBlock = Schema.Struct({ @@ -869,5 +869,8 @@ export class CraftApiClient extends ServiceMap.Service()("CraftA addComments, } }), - dependencies: [FetchHttpClient.layer], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(FetchHttpClient.layer), + ) +} diff --git a/packages/integrations/src/discord/api-client.ts b/packages/integrations/src/discord/api-client.ts index 51405b431..8237c3132 100644 --- a/packages/integrations/src/discord/api-client.ts +++ b/packages/integrations/src/discord/api-client.ts @@ -1,5 +1,5 @@ import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http" -import { ServiceMap, Duration, Effect, Schema } from "effect" +import { ServiceMap, Duration, Effect, Layer, Schema } from "effect" export const DiscordAccountInfo = Schema.Struct({ externalAccountId: Schema.String, @@ -547,5 +547,8 @@ export class DiscordApiClient extends ServiceMap.Service()("Di createThread, } }), - dependencies: [FetchHttpClient.layer], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(FetchHttpClient.layer), + ) +} diff --git a/packages/integrations/src/github/api-client.ts b/packages/integrations/src/github/api-client.ts index 0036f9e92..603ff66ba 100644 --- a/packages/integrations/src/github/api-client.ts +++ b/packages/integrations/src/github/api-client.ts @@ -1,5 +1,5 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" -import { ServiceMap, Duration, Effect, Schedule, Schema } from "effect" +import { ServiceMap, Duration, Effect, Layer, Schedule, Schema } from "effect" /** * GitHub PR URL patterns: @@ -38,7 +38,7 @@ export const GitHubPR = Schema.Struct({ number: Schema.Number, title: Schema.String, body: Schema.NullOr(Schema.String), - state: Schema.Literal("open", "closed"), + state: Schema.Literals(["open", "closed"]), draft: Schema.Boolean, merged: Schema.Boolean, author: Schema.NullOr(GitHubPRAuthor), @@ -825,8 +825,11 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH getAccountInfo: wrappedGetAccountInfo, } }), - dependencies: [FetchHttpClient.layer], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(FetchHttpClient.layer), + ) +} // ============================================================================ // Legacy exports for backwards compatibility diff --git a/packages/integrations/src/github/jwt-service.ts b/packages/integrations/src/github/jwt-service.ts index c5ae9b13d..4be224896 100644 --- a/packages/integrations/src/github/jwt-service.ts +++ b/packages/integrations/src/github/jwt-service.ts @@ -1,6 +1,6 @@ import { createPrivateKey } from "node:crypto" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" -import { ServiceMap, Config, Effect, Redacted, Schema } from "effect" +import { ServiceMap, Config, Effect, Layer, Redacted, Schema } from "effect" import { SignJWT } from "jose" // ============================================================================ @@ -287,5 +287,8 @@ export class GitHubAppJWTService extends ServiceMap.Service getInstallationToken, } }), - dependencies: [FetchHttpClient.layer], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(FetchHttpClient.layer), + ) +} diff --git a/packages/integrations/src/linear/api-client.ts b/packages/integrations/src/linear/api-client.ts index 05426100a..d1b227189 100644 --- a/packages/integrations/src/linear/api-client.ts +++ b/packages/integrations/src/linear/api-client.ts @@ -742,5 +742,8 @@ export class LinearApiClient extends ServiceMap.Service()("Line getAccountInfo, } }), - dependencies: [FetchHttpClient.layer], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(FetchHttpClient.layer), + ) +} diff --git a/packages/schema/src/avatar-url.ts b/packages/schema/src/avatar-url.ts index 1254992c4..f59f6b985 100644 --- a/packages/schema/src/avatar-url.ts +++ b/packages/schema/src/avatar-url.ts @@ -77,7 +77,7 @@ export const AvatarUrl = Schema.String.pipe( Effect.catch((e) => Effect.succeed(e.message)), ), ), -).annotations({ +).annotate({ description: "A validated URL to an avatar image", title: "Avatar URL", }) diff --git a/packages/schema/src/ids.ts b/packages/schema/src/ids.ts index 115f7dd78..993c651fd 100644 --- a/packages/schema/src/ids.ts +++ b/packages/schema/src/ids.ts @@ -1,6 +1,6 @@ import { Schema } from "effect" -export const ChannelId = Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelId")).annotations({ +export const ChannelId = Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelId")).annotate({ description: "The ID of the channel where the message is posted", title: "Channel ID", }) @@ -8,7 +8,7 @@ export type ChannelId = Schema.Schema.Type export const ConnectConversationId = Schema.UUID.pipe( Schema.brand("@HazelChat/ConnectConversationId"), -).annotations({ +).annotate({ description: "The ID of a Hazel Connect conversation", title: "Connect Conversation ID", }) @@ -16,13 +16,13 @@ export type ConnectConversationId = Schema.Schema.Type -export const ConnectInviteId = Schema.UUID.pipe(Schema.brand("@HazelChat/ConnectInviteId")).annotations({ +export const ConnectInviteId = Schema.UUID.pipe(Schema.brand("@HazelChat/ConnectInviteId")).annotate({ description: "The ID of a Hazel Connect invite", title: "Connect Invite ID", }) @@ -30,31 +30,31 @@ export type ConnectInviteId = Schema.Schema.Type export const ConnectParticipantId = Schema.UUID.pipe( Schema.brand("@HazelChat/ConnectParticipantId"), -).annotations({ +).annotate({ description: "The ID of a Hazel Connect participant projection", title: "Connect Participant ID", }) export type ConnectParticipantId = Schema.Schema.Type -export const UserId = Schema.UUID.pipe(Schema.brand("@HazelChat/UserId")).annotations({ +export const UserId = Schema.UUID.pipe(Schema.brand("@HazelChat/UserId")).annotate({ description: "The ID of a user", title: "UserId ID", }) export type UserId = Schema.Schema.Type -export const BotId = Schema.UUID.pipe(Schema.brand("@HazelChat/BotId")).annotations({ +export const BotId = Schema.UUID.pipe(Schema.brand("@HazelChat/BotId")).annotate({ description: "The ID of a bot", title: "Bot ID", }) export type BotId = Schema.Schema.Type -export const MessageId = Schema.UUID.pipe(Schema.brand("@HazelChat/MessageId")).annotations({ +export const MessageId = Schema.UUID.pipe(Schema.brand("@HazelChat/MessageId")).annotate({ description: "The ID of the message being replied to", title: "Reply To Message ID", }) export type MessageId = Schema.Schema.Type -export const MessageReactionId = Schema.UUID.pipe(Schema.brand("@HazelChat/MessageReactionId")).annotations({ +export const MessageReactionId = Schema.UUID.pipe(Schema.brand("@HazelChat/MessageReactionId")).annotate({ description: "The ID of the message reaction", title: "Message Reaction ID", }) @@ -62,43 +62,43 @@ export type MessageReactionId = Schema.Schema.Type export const MessageAttachmentId = Schema.UUID.pipe( Schema.brand("@HazelChat/MessageAttachmentId"), -).annotations({ +).annotate({ description: "The ID of the message attachment", title: "Message Attachment ID", }) export type MessageAttachmentId = Schema.Schema.Type -export const AttachmentId = Schema.UUID.pipe(Schema.brand("@HazelChat/AttachmentId")).annotations({ +export const AttachmentId = Schema.UUID.pipe(Schema.brand("@HazelChat/AttachmentId")).annotate({ description: "The ID of the attachment being replied to", title: "Attachment ID", }) export type AttachmentId = Schema.Schema.Type -export const OrganizationId = Schema.UUID.pipe(Schema.brand("@HazelChat/OrganizationId")).annotations({ +export const OrganizationId = Schema.UUID.pipe(Schema.brand("@HazelChat/OrganizationId")).annotate({ description: "The ID of the organization", title: "Organization ID", }) export type OrganizationId = Schema.Schema.Type -export const InvitationId = Schema.UUID.pipe(Schema.brand("@HazelChat/InvitationId")).annotations({ +export const InvitationId = Schema.UUID.pipe(Schema.brand("@HazelChat/InvitationId")).annotate({ description: "The ID of the invitation", title: "Invitation ID", }) export type InvitationId = Schema.Schema.Type -export const PinnedMessageId = Schema.UUID.pipe(Schema.brand("@HazelChat/PinnedMessageId")).annotations({ +export const PinnedMessageId = Schema.UUID.pipe(Schema.brand("@HazelChat/PinnedMessageId")).annotate({ description: "The ID of the pinned message", title: "Pinned Message ID", }) export type PinnedMessageId = Schema.Schema.Type -export const NotificationId = Schema.UUID.pipe(Schema.brand("@HazelChat/NotificationId")).annotations({ +export const NotificationId = Schema.UUID.pipe(Schema.brand("@HazelChat/NotificationId")).annotate({ description: "The ID of the notification", title: "Notification ID", }) export type NotificationId = Schema.Schema.Type -export const ChannelMemberId = Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelMemberId")).annotations({ +export const ChannelMemberId = Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelMemberId")).annotate({ description: "The ID of the channel member", title: "Channel Member ID", }) @@ -106,13 +106,13 @@ export type ChannelMemberId = Schema.Schema.Type export const OrganizationMemberId = Schema.UUID.pipe( Schema.brand("@HazelChat/OrganizationMemberId"), -).annotations({ +).annotate({ description: "The ID of the organization member", title: "Organization Member ID", }) export type OrganizationMemberId = Schema.Schema.Type -export const TypingIndicatorId = Schema.UUID.pipe(Schema.brand("@HazelChat/TypingIndicatorId")).annotations({ +export const TypingIndicatorId = Schema.UUID.pipe(Schema.brand("@HazelChat/TypingIndicatorId")).annotate({ description: "The ID of the typing indicator", title: "Typing Indicator ID", }) @@ -120,7 +120,7 @@ export type TypingIndicatorId = Schema.Schema.Type export const UserPresenceStatusId = Schema.UUID.pipe( Schema.brand("@HazelChat/UserPresenceStatusId"), -).annotations({ +).annotate({ description: "The ID of the user presence status", title: "User Presence Status ID", }) @@ -128,13 +128,13 @@ export type UserPresenceStatusId = Schema.Schema.Type -export const SyncConnectionId = Schema.UUID.pipe(Schema.brand("@HazelChat/SyncConnectionId")).annotations({ +export const SyncConnectionId = Schema.UUID.pipe(Schema.brand("@HazelChat/SyncConnectionId")).annotate({ description: "The ID of a chat sync connection", title: "Sync Connection ID", }) @@ -164,25 +164,25 @@ export const ExternalWebhookId = Schema.String.pipe(Schema.brand("@HazelChat/Ext ) export type ExternalWebhookId = Schema.Schema.Type -export const ExternalUserId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalUserId")).annotations({ +export const ExternalUserId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalUserId")).annotate({ description: "The external user identifier from a synced provider", title: "External User ID", }) export type ExternalUserId = Schema.Schema.Type -export const ExternalThreadId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalThreadId")).annotations({ +export const ExternalThreadId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalThreadId")).annotate({ description: "The external thread identifier from a synced provider", title: "External Thread ID", }) export type ExternalThreadId = Schema.Schema.Type -export const SyncChannelLinkId = Schema.UUID.pipe(Schema.brand("@HazelChat/SyncChannelLinkId")).annotations({ +export const SyncChannelLinkId = Schema.UUID.pipe(Schema.brand("@HazelChat/SyncChannelLinkId")).annotate({ description: "The ID of a chat sync channel link", title: "Sync Channel Link ID", }) export type SyncChannelLinkId = Schema.Schema.Type -export const SyncMessageLinkId = Schema.UUID.pipe(Schema.brand("@HazelChat/SyncMessageLinkId")).annotations({ +export const SyncMessageLinkId = Schema.UUID.pipe(Schema.brand("@HazelChat/SyncMessageLinkId")).annotate({ description: "The ID of a chat sync message link", title: "Sync Message Link ID", }) @@ -206,19 +206,19 @@ export type IntegrationTokenId = Schema.Schema.Type export const MessageIntegrationLinkId = Schema.UUID.pipe( Schema.brand("@HazelChat/MessageIntegrationLinkId"), -).annotations({ +).annotate({ description: "The ID of a message-integration link", title: "Message Integration Link ID", }) export type MessageIntegrationLinkId = Schema.Schema.Type -export const ChannelWebhookId = Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelWebhookId")).annotations({ +export const ChannelWebhookId = Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelWebhookId")).annotate({ description: "The ID of a channel webhook", title: "Channel Webhook ID", }) export type ChannelWebhookId = Schema.Schema.Type -export const ChannelIcon = Schema.String.pipe(Schema.brand("@HazelChat/ChannelIcon")).annotations({ +export const ChannelIcon = Schema.String.pipe(Schema.brand("@HazelChat/ChannelIcon")).annotate({ description: "An emoji icon for a channel", title: "Channel Icon", }) @@ -226,31 +226,31 @@ export type ChannelIcon = Schema.Schema.Type export const GitHubSubscriptionId = Schema.UUID.pipe( Schema.brand("@HazelChat/GitHubSubscriptionId"), -).annotations({ +).annotate({ description: "The ID of a GitHub subscription", title: "GitHub Subscription ID", }) export type GitHubSubscriptionId = Schema.Schema.Type -export const BotCommandId = Schema.UUID.pipe(Schema.brand("@HazelChat/BotCommandId")).annotations({ +export const BotCommandId = Schema.UUID.pipe(Schema.brand("@HazelChat/BotCommandId")).annotate({ description: "The ID of a bot command", title: "Bot Command ID", }) export type BotCommandId = Schema.Schema.Type -export const BotInstallationId = Schema.UUID.pipe(Schema.brand("@HazelChat/BotInstallationId")).annotations({ +export const BotInstallationId = Schema.UUID.pipe(Schema.brand("@HazelChat/BotInstallationId")).annotate({ description: "The ID of a bot installation", title: "Bot Installation ID", }) export type BotInstallationId = Schema.Schema.Type -export const ChannelSectionId = Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelSectionId")).annotations({ +export const ChannelSectionId = Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelSectionId")).annotate({ description: "The ID of a channel section", title: "Channel Section ID", }) export type ChannelSectionId = Schema.Schema.Type -export const RssSubscriptionId = Schema.UUID.pipe(Schema.brand("@HazelChat/RssSubscriptionId")).annotations({ +export const RssSubscriptionId = Schema.UUID.pipe(Schema.brand("@HazelChat/RssSubscriptionId")).annotate({ description: "The ID of an RSS subscription", title: "RSS Subscription ID", }) @@ -258,13 +258,13 @@ export type RssSubscriptionId = Schema.Schema.Type export const IntegrationRequestId = Schema.UUID.pipe( Schema.brand("@HazelChat/IntegrationRequestId"), -).annotations({ +).annotate({ description: "The ID of an integration request", title: "Integration Request ID", }) export type IntegrationRequestId = Schema.Schema.Type -export const CustomEmojiId = Schema.UUID.pipe(Schema.brand("@HazelChat/CustomEmojiId")).annotations({ +export const CustomEmojiId = Schema.UUID.pipe(Schema.brand("@HazelChat/CustomEmojiId")).annotate({ description: "The ID of a custom emoji", title: "Custom Emoji ID", }) @@ -272,7 +272,7 @@ export type CustomEmojiId = Schema.Schema.Type export const MessageOutboxEventId = Schema.UUID.pipe( Schema.brand("@HazelChat/MessageOutboxEventId"), -).annotations({ +).annotate({ description: "The ID of a message outbox event", title: "Message Outbox Event ID", }) diff --git a/packages/schema/src/workos.ts b/packages/schema/src/workos.ts index bae246fe1..fa370d37f 100644 --- a/packages/schema/src/workos.ts +++ b/packages/schema/src/workos.ts @@ -2,7 +2,7 @@ import { Schema } from "effect" export const WorkOSUserId = Schema.NonEmptyTrimmedString.pipe( Schema.brand("@HazelChat/WorkOSUserId"), -).annotations({ +).annotate({ description: "A WorkOS user identifier", title: "WorkOS User ID", }) @@ -10,7 +10,7 @@ export type WorkOSUserId = Schema.Schema.Type export const WorkOSOrganizationId = Schema.NonEmptyTrimmedString.pipe( Schema.brand("@HazelChat/WorkOSOrganizationId"), -).annotations({ +).annotate({ description: "A WorkOS organization identifier", title: "WorkOS Organization ID", }) @@ -18,7 +18,7 @@ export type WorkOSOrganizationId = Schema.Schema.Type export const WorkOSInvitationId = Schema.NonEmptyTrimmedString.pipe( Schema.brand("@HazelChat/WorkOSInvitationId"), -).annotations({ +).annotate({ description: "A WorkOS invitation identifier", title: "WorkOS Invitation ID", }) @@ -34,13 +34,13 @@ export type WorkOSInvitationId = Schema.Schema.Type export const WorkOSClientId = Schema.NonEmptyTrimmedString.pipe( Schema.brand("@HazelChat/WorkOSClientId"), -).annotations({ +).annotate({ description: "A WorkOS client identifier", title: "WorkOS Client ID", }) export type WorkOSClientId = Schema.Schema.Type -export const WorkOSRole = Schema.Literal("admin", "member", "owner") +export const WorkOSRole = Schema.Literals(["admin", "member", "owner"]) export type WorkOSRole = Schema.Schema.Type export class WorkOSJwtClaims extends Schema.Class("WorkOSJwtClaims")({ diff --git a/packages/setup/src/services/cert-manager.ts b/packages/setup/src/services/cert-manager.ts index d5a50a662..d883a7c27 100644 --- a/packages/setup/src/services/cert-manager.ts +++ b/packages/setup/src/services/cert-manager.ts @@ -105,4 +105,6 @@ export class CertManager extends ServiceMap.Service()("CertManager" catch: (e) => new Error(`Cert generation failed: ${e}`), }), }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/setup/src/services/doctor.ts b/packages/setup/src/services/doctor.ts index b549fd0b0..7912815ec 100644 --- a/packages/setup/src/services/doctor.ts +++ b/packages/setup/src/services/doctor.ts @@ -129,4 +129,6 @@ export class Doctor extends ServiceMap.Service()("Doctor", { return { environment, services } }), }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/setup/src/services/env-writer.ts b/packages/setup/src/services/env-writer.ts index f3c90a652..5a9359678 100644 --- a/packages/setup/src/services/env-writer.ts +++ b/packages/setup/src/services/env-writer.ts @@ -134,4 +134,6 @@ export class EnvWriter extends ServiceMap.Service()("EnvWriter", { return result }), }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/setup/src/services/secrets.ts b/packages/setup/src/services/secrets.ts index 3d172e789..a345f05eb 100644 --- a/packages/setup/src/services/secrets.ts +++ b/packages/setup/src/services/secrets.ts @@ -14,4 +14,6 @@ export class SecretGenerator extends ServiceMap.Service()("Secr return Buffer.from(bytes).toString("base64") }, }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/setup/src/services/validators.ts b/packages/setup/src/services/validators.ts index f36789469..63a646cf6 100644 --- a/packages/setup/src/services/validators.ts +++ b/packages/setup/src/services/validators.ts @@ -55,4 +55,6 @@ export class CredentialValidator extends ServiceMap.Service }), }), }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} From b7cc83126cb965a9113c9d44cf9f66b7c6042949 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 00:19:24 +0100 Subject: [PATCH 03/34] stuff --- apps/backend/src/lib/create-transactionId.ts | 2 +- apps/backend/src/lib/schema.ts | 2 +- .../src/services/oauth/oauth-http-client.ts | 2 +- libs/ai-openrouter/src/OpenRouterClient.ts | 40 +-- libs/bot-sdk/src/hazel-bot-sdk.ts | 2 +- packages/db/src/services/database.ts | 2 +- packages/db/src/services/drizzle-effect.ts | 2 +- packages/domain/src/bot-gateway.ts | 2 +- packages/domain/src/current-user.ts | 2 +- packages/domain/src/error-utils.ts | 2 +- packages/domain/src/errors.ts | 2 +- packages/domain/src/http/api-v1/messages.ts | 4 +- .../domain/src/http/integration-resources.ts | 4 +- packages/domain/src/http/klipy.ts | 8 +- packages/domain/src/http/uploads.ts | 10 +- .../domain/src/models/message-embed-schema.ts | 30 +- packages/domain/src/models/utils.ts | 265 ++++++++---------- packages/domain/src/rpc/bots.ts | 8 +- packages/domain/src/rpc/channel-webhooks.ts | 8 +- .../integrations/src/discord/api-client.ts | 6 +- .../integrations/src/github/api-client.ts | 12 +- .../integrations/src/github/jwt-service.ts | 2 +- packages/schema/src/avatar-url.ts | 2 +- 23 files changed, 193 insertions(+), 226 deletions(-) diff --git a/apps/backend/src/lib/create-transactionId.ts b/apps/backend/src/lib/create-transactionId.ts index f77b64e2b..2cd40ee0e 100644 --- a/apps/backend/src/lib/create-transactionId.ts +++ b/apps/backend/src/lib/create-transactionId.ts @@ -30,7 +30,7 @@ export const generateTransactionId = Effect.fn("generateTransactionId")(function Effect.tap((rawTxid) => Effect.log(`[txid-debug] Raw PostgreSQL txid string: "${rawTxid}", type: ${typeof rawTxid}`), ), - Effect.flatMap((txid) => Schema.decode(TransactionIdFromString)(txid)), + Effect.flatMap((txid) => Schema.decodeEffect(TransactionIdFromString)(txid)), Effect.tap((decodedTxid) => Effect.log(`[txid-debug] Decoded transactionId: ${decodedTxid}, type: ${typeof decodedTxid}`), ), diff --git a/apps/backend/src/lib/schema.ts b/apps/backend/src/lib/schema.ts index 4a8dee3a8..c3dcaeb5c 100644 --- a/apps/backend/src/lib/schema.ts +++ b/apps/backend/src/lib/schema.ts @@ -1,7 +1,7 @@ import { Schema } from "effect" export const RelativeUrl = Schema.String.pipe( - Schema.nonEmptyString(), + Schema.isNonEmpty(), Schema.startsWith("/"), Schema.filter((url) => !url.startsWith("//"), { message: () => "Protocol-relative URLs are not allowed", diff --git a/apps/backend/src/services/oauth/oauth-http-client.ts b/apps/backend/src/services/oauth/oauth-http-client.ts index 201df813f..1faec0c86 100644 --- a/apps/backend/src/services/oauth/oauth-http-client.ts +++ b/apps/backend/src/services/oauth/oauth-http-client.ts @@ -25,7 +25,7 @@ const OAuthTokenApiResponse = Schema.Struct({ refresh_token: Schema.optional(Schema.String), expires_in: Schema.optional(Schema.Number), scope: Schema.optional(Schema.String), - token_type: Schema.optionalWith(Schema.String, { default: () => "Bearer" }), + token_type: Schema.optional(Schema.String, { default: () => "Bearer" }), }) // ============================================================================ diff --git a/libs/ai-openrouter/src/OpenRouterClient.ts b/libs/ai-openrouter/src/OpenRouterClient.ts index 8749b3c11..39ddd6771 100644 --- a/libs/ai-openrouter/src/OpenRouterClient.ts +++ b/libs/ai-openrouter/src/OpenRouterClient.ts @@ -123,7 +123,7 @@ export const make: (options: { request: HttpClientRequest.HttpClientRequest, schema: Schema.Schema, ): Stream.Stream => { - const decodeEvent = Schema.decode(Schema.parseJson(schema)) + const decodeEvent = Schema.decodeEffect(Schema.parseJson(schema)) return httpClientOk.execute(request).pipe( Effect.map((r) => r.stream), Stream.unwrapScoped, @@ -314,10 +314,10 @@ export class ChatStreamingMessageToolCall extends Schema.Class( "@effect/ai-openrouter/ChatStreamingMessageChunk", )({ - role: Schema.optionalWith(Schema.Literal("assistant"), { nullable: true }), - content: Schema.optionalWith(Schema.String, { nullable: true }), - reasoning: Schema.optionalWith(Schema.String, { nullable: true }), - reasoning_details: Schema.optionalWith(Schema.Array(Generated.ReasoningDetail), { nullable: true }), - images: Schema.optionalWith(Schema.Array(Generated.ChatMessageContentItemImage), { nullable: true }), - refusal: Schema.optionalWith(Schema.String, { nullable: true }), - tool_calls: Schema.optionalWith(Schema.Array(ChatStreamingMessageToolCall), { nullable: true }), - annotations: Schema.optionalWith(Schema.Array(Generated.AnnotationDetail), { nullable: true }), + role: Schema.optional(Schema.NullOr(Schema.Literal("assistant"))), + content: Schema.optional(Schema.NullOr(Schema.String)), + reasoning: Schema.optional(Schema.NullOr(Schema.String)), + reasoning_details: Schema.optional(Schema.NullOr(Schema.Array(Generated.ReasoningDetail))), + images: Schema.optional(Schema.NullOr(Schema.Array(Generated.ChatMessageContentItemImage))), + refusal: Schema.optional(Schema.NullOr(Schema.String)), + tool_calls: Schema.optional(Schema.NullOr(Schema.Array(ChatStreamingMessageToolCall))), + annotations: Schema.optional(Schema.NullOr(Schema.Array(Generated.AnnotationDetail))), }) {} /** @@ -347,10 +347,10 @@ export class ChatStreamingChoice extends Schema.Class( "@effect/ai-openrouter/ChatStreamingChoice", )({ index: Schema.Number, - delta: Schema.optionalWith(ChatStreamingMessageChunk, { nullable: true }), - finish_reason: Schema.optionalWith(Generated.ChatCompletionFinishReason, { nullable: true }), - native_finish_reason: Schema.optionalWith(Schema.String, { nullable: true }), - logprobs: Schema.optionalWith(Generated.ChatMessageTokenLogprobs, { nullable: true }), + delta: Schema.optional(Schema.NullOr(ChatStreamingMessageChunk)), + finish_reason: Schema.optional(Schema.NullOr(Generated.ChatCompletionFinishReason)), + native_finish_reason: Schema.optional(Schema.NullOr(Schema.String)), + logprobs: Schema.optional(Schema.NullOr(Generated.ChatMessageTokenLogprobs)), }) {} /** @@ -360,14 +360,14 @@ export class ChatStreamingChoice extends Schema.Class( export class ChatStreamingResponseChunk extends Schema.Class( "@effect/ai-openrouter/ChatStreamingResponseChunk", )({ - id: Schema.optionalWith(Schema.String, { nullable: true }), + id: Schema.optional(Schema.NullOr(Schema.String)), model: Schema.optionalWith(Schema.TemplateLiteral(Schema.String, Schema.Literal("/"), Schema.String), { nullable: true, }), - provider: Schema.optionalWith(Schema.String, { nullable: true }), + provider: Schema.optional(Schema.NullOr(Schema.String)), created: Schema.DateTimeUtcFromNumber, choices: Schema.Array(ChatStreamingChoice), - error: Schema.optionalWith(Generated.ChatError.fields.error, { nullable: true }), - system_fingerprint: Schema.optionalWith(Schema.String, { nullable: true }), - usage: Schema.optionalWith(Generated.ChatGenerationTokenUsage, { nullable: true }), + error: Schema.optional(Schema.NullOr(Generated.ChatError.fields.error)), + system_fingerprint: Schema.optional(Schema.NullOr(Schema.String)), + usage: Schema.optional(Schema.NullOr(Generated.ChatGenerationTokenUsage)), }) {} diff --git a/libs/bot-sdk/src/hazel-bot-sdk.ts b/libs/bot-sdk/src/hazel-bot-sdk.ts index f0622adc0..9229d3b4e 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.ts @@ -311,7 +311,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB ) const setBotState = (key: string, schema: Schema.Schema, value: A) => - Schema.encode(schema)(value).pipe( + Schema.encodeEffect(schema)(value).pipe( Effect.flatMap((encoded) => botStateStore.set(authContext.botId as BotId, key, JSON.stringify(encoded)), ), diff --git a/packages/db/src/services/database.ts b/packages/db/src/services/database.ts index dd8d875be..35151678f 100644 --- a/packages/db/src/services/database.ts +++ b/packages/db/src/services/database.ts @@ -192,7 +192,7 @@ const makeService = (config: Config) => ) => Effect.Effect, ): Effect.Effect => { return Effect.gen(function* () { - const validatedInput = yield* Schema.decode(inputSchema)(rawData) + const validatedInput = yield* Schema.decodeEffect(inputSchema)(rawData) if (tx) { return yield* queryFn(tx, validatedInput) diff --git a/packages/db/src/services/drizzle-effect.ts b/packages/db/src/services/drizzle-effect.ts index 765d7dc0f..fc25c0beb 100644 --- a/packages/db/src/services/drizzle-effect.ts +++ b/packages/db/src/services/drizzle-effect.ts @@ -293,7 +293,7 @@ function mapColumnToSchema(column: Drizzle.Column): Schema.Schema { Drizzle.is(column, DrizzleSqlite.SQLiteText)) && typeof column.length === "number" ) { - sType = sType.pipe(Schema.maxLength(column.length)) + sType = sType.pipe(Schema.isMaxLength(column.length)) } type = sType } diff --git a/packages/domain/src/bot-gateway.ts b/packages/domain/src/bot-gateway.ts index e9a3f7a4c..656dc7d14 100644 --- a/packages/domain/src/bot-gateway.ts +++ b/packages/domain/src/bot-gateway.ts @@ -9,7 +9,7 @@ export const BotGatewayCommandInvokePayload = Schema.Struct({ channelId: ChannelId, userId: UserId, orgId: OrganizationId, - arguments: Schema.Record({ key: Schema.String, value: Schema.String }), + arguments: Schema.Record(Schema.String, Schema.String), timestamp: Schema.Number, }) export type BotGatewayCommandInvokePayload = Schema.Schema.Type diff --git a/packages/domain/src/current-user.ts b/packages/domain/src/current-user.ts index 697eb1ebc..df2331dd4 100644 --- a/packages/domain/src/current-user.ts +++ b/packages/domain/src/current-user.ts @@ -17,7 +17,7 @@ import { export class Schema extends S.Class("CurrentUserSchema")({ id: UserId, organizationId: S.NullishOr(OrganizationId), - role: S.Literal("admin", "member", "owner"), + role: S.Literals(["admin", "member", "owner"]), avatarUrl: S.optional(S.String), firstName: S.optional(S.String), lastName: S.optional(S.String), diff --git a/packages/domain/src/error-utils.ts b/packages/domain/src/error-utils.ts index 3f1c86951..f0381ee65 100644 --- a/packages/domain/src/error-utils.ts +++ b/packages/domain/src/error-utils.ts @@ -26,7 +26,7 @@ export const refailUnauthorized = (entity: string, action: string) => { effect, (e) => !UnauthorizedError.is(e), (e) => - Effect.flatMap(CurrentUser.Context, (actor) => { + CurrentUser.Context.use((actor) => { // Convert PermissionError to UnauthorizedError with scope info if (PermissionError.is(e)) { return Effect.fail( diff --git a/packages/domain/src/errors.ts b/packages/domain/src/errors.ts index 0a108e908..3d1bb2363 100644 --- a/packages/domain/src/errors.ts +++ b/packages/domain/src/errors.ts @@ -96,7 +96,7 @@ export class WorkflowServiceUnavailableError extends Schema.TaggedErrorClass("ListMess limit: Schema.optional( Schema.NumberFromString.pipe( Schema.int(), - Schema.greaterThanOrEqualTo(1), - Schema.lessThanOrEqualTo(100), + Schema.isGreaterThanOrEqualTo(1), + Schema.isLessThanOrEqualTo(100), ), ), }) {} diff --git a/packages/domain/src/http/integration-resources.ts b/packages/domain/src/http/integration-resources.ts index 515c071f8..edabbe344 100644 --- a/packages/domain/src/http/integration-resources.ts +++ b/packages/domain/src/http/integration-resources.ts @@ -226,8 +226,8 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res ) .setUrlParams( Schema.Struct({ - page: Schema.optionalWith(Schema.NumberFromString, { default: () => 1 }), - perPage: Schema.optionalWith(Schema.NumberFromString, { default: () => 30 }), + page: Schema.optional(Schema.NumberFromString, { default: () => 1 }), + perPage: Schema.optional(Schema.NumberFromString, { default: () => 30 }), }), ) .annotateContext( diff --git a/packages/domain/src/http/klipy.ts b/packages/domain/src/http/klipy.ts index 8bfafc401..0269c34fa 100644 --- a/packages/domain/src/http/klipy.ts +++ b/packages/domain/src/http/klipy.ts @@ -72,8 +72,8 @@ export class KlipyGroup extends HttpApiGroup.make("klipy") HttpApiEndpoint.get("trending", "/trending") .setUrlParams( Schema.Struct({ - page: Schema.optionalWith(Schema.NumberFromString, { default: () => 1 }), - per_page: Schema.optionalWith(Schema.NumberFromString, { default: () => 25 }), + page: Schema.optional(Schema.NumberFromString, { default: () => 1 }), + per_page: Schema.optional(Schema.NumberFromString, { default: () => 25 }), }), ) .addSuccess(KlipySearchResponse) @@ -85,8 +85,8 @@ export class KlipyGroup extends HttpApiGroup.make("klipy") .setUrlParams( Schema.Struct({ q: Schema.String, - page: Schema.optionalWith(Schema.NumberFromString, { default: () => 1 }), - per_page: Schema.optionalWith(Schema.NumberFromString, { default: () => 25 }), + page: Schema.optional(Schema.NumberFromString, { default: () => 1 }), + per_page: Schema.optional(Schema.NumberFromString, { default: () => 25 }), }), ) .addSuccess(KlipySearchResponse) diff --git a/packages/domain/src/http/uploads.ts b/packages/domain/src/http/uploads.ts index e4b2b9996..65eccc471 100644 --- a/packages/domain/src/http/uploads.ts +++ b/packages/domain/src/http/uploads.ts @@ -47,7 +47,7 @@ export class UserAvatarUploadRequest extends Schema.Class "File size must be between 1 byte and 5MB", }), ), @@ -66,7 +66,7 @@ export class BotAvatarUploadRequest extends Schema.Class }), ), fileSize: Schema.Number.pipe( - Schema.between(1, MAX_AVATAR_SIZE, { + Schema.isBetween(1, MAX_AVATAR_SIZE, { message: () => "File size must be between 1 byte and 5MB", }), ), @@ -86,7 +86,7 @@ export class OrganizationAvatarUploadRequest extends Schema.Class "File size must be between 1 byte and 5MB", }), ), @@ -101,7 +101,7 @@ export class AttachmentUploadRequest extends Schema.Class "File size must be between 1 byte and 10MB", }), ), @@ -124,7 +124,7 @@ export class CustomEmojiUploadRequest extends Schema.Class "File size must be between 1 byte and 256KB", }), ), diff --git a/packages/domain/src/models/message-embed-schema.ts b/packages/domain/src/models/message-embed-schema.ts index 086712dd3..a0b91fbcb 100644 --- a/packages/domain/src/models/message-embed-schema.ts +++ b/packages/domain/src/models/message-embed-schema.ts @@ -2,16 +2,16 @@ import { Schema } from "effect" // Embed author section export const MessageEmbedAuthor = Schema.Struct({ - name: Schema.String.pipe(Schema.maxLength(256)), - url: Schema.optional(Schema.String.pipe(Schema.maxLength(2048))), - iconUrl: Schema.optional(Schema.String.pipe(Schema.maxLength(2048))), + name: Schema.String.pipe(Schema.isMaxLength(256)), + url: Schema.optional(Schema.String.pipe(Schema.isMaxLength(2048))), + iconUrl: Schema.optional(Schema.String.pipe(Schema.isMaxLength(2048))), }) export type MessageEmbedAuthor = Schema.Schema.Type // Embed footer section export const MessageEmbedFooter = Schema.Struct({ - text: Schema.String.pipe(Schema.maxLength(2048)), - iconUrl: Schema.optional(Schema.String.pipe(Schema.maxLength(2048))), + text: Schema.String.pipe(Schema.isMaxLength(2048)), + iconUrl: Schema.optional(Schema.String.pipe(Schema.isMaxLength(2048))), }) export type MessageEmbedFooter = Schema.Schema.Type @@ -39,8 +39,8 @@ export type MessageEmbedFieldOptions = Schema.Schema.Type // Embed badge (for status indicators) export const MessageEmbedBadge = Schema.Struct({ - text: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(64)), - color: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.between(0, 16777215))), // 0x000000 to 0xFFFFFF + text: Schema.String.pipe(Schema.isMinLength(1), Schema.isMaxLength(64)), + color: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.isBetween(0, 16777215))), // 0x000000 to 0xFFFFFF }) export type MessageEmbedBadge = Schema.Schema.Type @@ -104,14 +104,14 @@ export type MessageEmbedLiveState = Schema.Schema.Type>(self: A) => A[Variant export const Override: (value: A) => A & Brand<"Override"> = VariantSchema.Override export interface Generated< - S extends Schema.Schema.All | Schema.PropertySignature.All, + S extends Schema.Top, > extends VariantSchema.Field<{ readonly select: S readonly update: S @@ -79,7 +78,7 @@ export interface Generated< }> {} /** A field for database-generated columns (available for select and update, not insert). */ -export const Generated = ( +export const Generated = ( schema: S, ): Generated => Field({ @@ -88,12 +87,12 @@ export const Generated = extends VariantSchema.Field<{ +export interface GeneratedOptional extends VariantSchema.Field<{ readonly select: S - readonly insert: Schema.optionalWith + readonly insert: Schema.optionalKey readonly update: S readonly json: S - readonly jsonCreate: Schema.optionalWith + readonly jsonCreate: Schema.optionalKey }> {} /** @@ -102,17 +101,17 @@ export interface GeneratedOptional extends VariantS * - Required for select, update, and json * - Optional for insert and jsonCreate (if not provided, DB generates the value) */ -export const GeneratedOptional = (schema: S): GeneratedOptional => +export const GeneratedOptional = (schema: S): GeneratedOptional => Field({ select: schema, - insert: Schema.optionalWith(schema, { exact: true }), + insert: Schema.optionalKey(schema), update: schema, json: schema, - jsonCreate: Schema.optionalWith(schema, { exact: true }), + jsonCreate: Schema.optionalKey(schema), }) export interface GeneratedByApp< - S extends Schema.Schema.All | Schema.PropertySignature.All, + S extends Schema.Top, > extends VariantSchema.Field<{ readonly select: S readonly insert: S @@ -121,7 +120,7 @@ export interface GeneratedByApp< }> {} /** A field for application-generated columns (required for DB variants, optional for JSON). */ -export const GeneratedByApp = ( +export const GeneratedByApp = ( schema: S, ): GeneratedByApp => Field({ @@ -132,7 +131,7 @@ export const GeneratedByApp = extends VariantSchema.Field<{ readonly select: S readonly insert: S @@ -140,7 +139,7 @@ export interface Sensitive< }> {} /** A field for sensitive values hidden from JSON variants. */ -export const Sensitive = ( +export const Sensitive = ( schema: S, ): Sensitive => Field({ @@ -149,167 +148,143 @@ export const Sensitive = extends VariantSchema.Field<{ +export interface FieldOption extends VariantSchema.Field<{ readonly select: Schema.OptionFromNullOr readonly insert: Schema.OptionFromNullOr readonly update: Schema.OptionFromNullOr - readonly json: Schema.optionalWith - readonly jsonCreate: Schema.optionalWith - readonly jsonUpdate: Schema.optionalWith + readonly json: Schema.OptionFromOptionalNullOr + readonly jsonCreate: Schema.OptionFromOptionalNullOr + readonly jsonUpdate: Schema.OptionFromOptionalNullOr }> {} /** Makes a field optional for all variants (nullable for DB, optional for JSON). */ -export const FieldOption: | Schema.Schema.Any>( +export const FieldOption: | Schema.Top>( self: Field, -) => Field extends Schema.Schema.Any +) => Field extends Schema.Top ? FieldOption : Field extends VariantSchema.Field ? VariantSchema.Field<{ - readonly [K in keyof S]: S[K] extends Schema.Schema.Any + readonly [K in keyof S]: S[K] extends Schema.Top ? K extends VariantsDatabase ? Schema.OptionFromNullOr - : Schema.optionalWith + : Schema.OptionFromOptionalNullOr : never }> : never = fieldEvolve({ select: Schema.OptionFromNullOr, insert: Schema.OptionFromNullOr, update: Schema.OptionFromNullOr, - json: Schema.optionalWith({ as: "Option" }), - jsonCreate: Schema.optionalWith({ as: "Option", nullable: true }), - jsonUpdate: Schema.optionalWith({ as: "Option", nullable: true }), + json: (s: any) => Schema.OptionFromOptionalNullOr(s), + jsonCreate: (s: any) => Schema.OptionFromOptionalNullOr(s), + jsonUpdate: (s: any) => Schema.OptionFromOptionalNullOr(s), }) as any -export interface DateTimeFromDate extends Schema.transform< - typeof Schema.ValidDateFromSelf, - typeof Schema.DateTimeUtcFromSelf -> {} +export interface DateTimeFromDate extends Schema.DateTimeUtcFromDate {} -export const DateTimeFromDate: DateTimeFromDate = Schema.transform( - Schema.ValidDateFromSelf, - Schema.DateTimeUtcFromSelf, - { - decode: DateTime.unsafeFromDate, - encode: DateTime.toDateUtc, - }, -) +export const DateTimeFromDate: DateTimeFromDate = Schema.DateTimeUtcFromDate -export interface Date extends Schema.transformOrFail< - typeof Schema.String, - typeof Schema.DateTimeUtcFromSelf +export interface Date extends Schema.decodeTo< + Schema.DateTimeUtc, + Schema.String > {} /** A DateTime.Utc serialized as ISO date string (YYYY-MM-DD). */ -export const Date: Date = Schema.transformOrFail(Schema.String, Schema.DateTimeUtcFromSelf, { - decode: (s, _, ast) => - DateTime.make(s).pipe( - Option.map(DateTime.removeTime), - Option.match({ - onNone: () => ParseResult.fail(new ParseResult.Type(ast, s)), - onSome: (dt) => ParseResult.succeed(dt), - }), - ), - encode: (dt) => ParseResult.succeed(DateTime.formatIsoDate(dt)), -}) - -export const DateWithNow = VariantSchema.Overrideable(Date, Schema.DateTimeUtcFromSelf, { - generate: Option.match({ - onNone: () => Effect.map(DateTime.now, DateTime.removeTime), - onSome: (dt) => Effect.succeed(DateTime.removeTime(dt)), +export const Date: Date = Schema.String.pipe( + Schema.decodeTo(Schema.DateTimeUtc, { + decode: (s, _, ast) => + DateTime.make(s).pipe( + (opt) => { + if (opt._tag === "Some") { + return Effect.succeed(DateTime.removeTime(opt.value)) + } + return Effect.fail(new SchemaIssue.InvalidValue(ast, s)) + }, + ), + encode: (dt) => Effect.succeed(DateTime.formatIsoDate(dt)), }), +) as any + +export const DateWithNow = VariantSchema.Overrideable(Date as any, { + defaultValue: Effect.map(DateTime.now, DateTime.removeTime), }) -export const DateTimeWithNow = VariantSchema.Overrideable(Schema.String, Schema.DateTimeUtcFromSelf, { - generate: Option.match({ - onNone: () => Effect.map(DateTime.now, DateTime.formatIso), - onSome: (dt) => Effect.succeed(DateTime.formatIso(dt)), - }), - decode: Schema.DateTimeUtc, +export const DateTimeWithNow = VariantSchema.Overrideable(Schema.DateTimeUtcFromString, { + defaultValue: DateTime.now, }) export const DateTimeFromDateWithNow = VariantSchema.Overrideable( - Schema.DateFromSelf, - Schema.DateTimeUtcFromSelf, + Schema.DateTimeUtcFromDate, { - generate: Option.match({ - onNone: () => Effect.map(DateTime.now, DateTime.toDateUtc), - onSome: (dt) => Effect.succeed(DateTime.toDateUtc(dt)), - }), - decode: DateTimeFromDate, + defaultValue: DateTime.now, }, ) export const DateTimeFromNumberWithNow = VariantSchema.Overrideable( - Schema.Number, - Schema.DateTimeUtcFromSelf, + Schema.DateTimeUtcFromMillis, { - generate: Option.match({ - onNone: () => Effect.map(DateTime.now, DateTime.toEpochMillis), - onSome: (dt) => Effect.succeed(DateTime.toEpochMillis(dt)), - }), - decode: Schema.DateTimeUtcFromNumber, + defaultValue: DateTime.now, }, ) export interface DateTimeInsert extends VariantSchema.Field<{ - readonly select: typeof Schema.DateTimeUtc - readonly insert: VariantSchema.Overrideable - readonly json: typeof Schema.DateTimeUtc + readonly select: typeof Schema.DateTimeUtcFromString + readonly insert: VariantSchema.Overrideable + readonly json: typeof Schema.DateTimeUtcFromString }> {} /** A DateTime.Utc field set on insert only, serialized as string (createdAt). */ export const DateTimeInsert: DateTimeInsert = Field({ - select: Schema.DateTimeUtc, + select: Schema.DateTimeUtcFromString, insert: DateTimeWithNow, - json: Schema.DateTimeUtc, + json: Schema.DateTimeUtcFromString, }) export interface DateTimeInsertFromDate extends VariantSchema.Field<{ readonly select: DateTimeFromDate - readonly insert: VariantSchema.Overrideable - readonly json: typeof Schema.DateTimeUtc + readonly insert: VariantSchema.Overrideable + readonly json: typeof Schema.DateTimeUtcFromString }> {} /** A DateTime.Utc field set on insert only, serialized as Date object. */ export const DateTimeInsertFromDate: DateTimeInsertFromDate = Field({ select: DateTimeFromDate, insert: DateTimeFromDateWithNow, - json: Schema.DateTimeUtc, + json: Schema.DateTimeUtcFromString, }) export interface DateTimeInsertFromNumber extends VariantSchema.Field<{ - readonly select: typeof Schema.DateTimeUtcFromNumber - readonly insert: VariantSchema.Overrideable - readonly json: typeof Schema.DateTimeUtcFromNumber + readonly select: typeof Schema.DateTimeUtcFromMillis + readonly insert: VariantSchema.Overrideable + readonly json: typeof Schema.DateTimeUtcFromMillis }> {} /** A DateTime.Utc field set on insert only, serialized as epoch milliseconds. */ export const DateTimeInsertFromNumber: DateTimeInsertFromNumber = Field({ - select: Schema.DateTimeUtcFromNumber, + select: Schema.DateTimeUtcFromMillis, insert: DateTimeFromNumberWithNow, - json: Schema.DateTimeUtcFromNumber, + json: Schema.DateTimeUtcFromMillis, }) export interface DateTimeUpdate extends VariantSchema.Field<{ - readonly select: typeof Schema.DateTimeUtc - readonly insert: VariantSchema.Overrideable - readonly update: VariantSchema.Overrideable - readonly json: typeof Schema.DateTimeUtc + readonly select: typeof Schema.DateTimeUtcFromString + readonly insert: VariantSchema.Overrideable + readonly update: VariantSchema.Overrideable + readonly json: typeof Schema.DateTimeUtcFromString }> {} /** A DateTime.Utc field set on insert/update, serialized as string (updatedAt). */ export const DateTimeUpdate: DateTimeUpdate = Field({ - select: Schema.DateTimeUtc, + select: Schema.DateTimeUtcFromString, insert: DateTimeWithNow, update: DateTimeWithNow, - json: Schema.DateTimeUtc, + json: Schema.DateTimeUtcFromString, }) export interface DateTimeUpdateFromDate extends VariantSchema.Field<{ readonly select: DateTimeFromDate - readonly insert: VariantSchema.Overrideable - readonly update: VariantSchema.Overrideable - readonly json: typeof Schema.DateTimeUtc + readonly insert: VariantSchema.Overrideable + readonly update: VariantSchema.Overrideable + readonly json: typeof Schema.DateTimeUtcFromString }> {} /** A DateTime.Utc field set on insert/update, serialized as Date object. */ @@ -317,40 +292,40 @@ export const DateTimeUpdateFromDate: DateTimeUpdateFromDate = Field({ select: DateTimeFromDate, insert: DateTimeFromDateWithNow, update: DateTimeFromDateWithNow, - json: Schema.DateTimeUtc, + json: Schema.DateTimeUtcFromString, }) export interface DateTimeUpdateFromNumber extends VariantSchema.Field<{ - readonly select: typeof Schema.DateTimeUtcFromNumber - readonly insert: VariantSchema.Overrideable - readonly update: VariantSchema.Overrideable - readonly json: typeof Schema.DateTimeUtcFromNumber + readonly select: typeof Schema.DateTimeUtcFromMillis + readonly insert: VariantSchema.Overrideable + readonly update: VariantSchema.Overrideable + readonly json: typeof Schema.DateTimeUtcFromMillis }> {} /** A DateTime.Utc field set on insert/update, serialized as epoch milliseconds. */ export const DateTimeUpdateFromNumber: DateTimeUpdateFromNumber = Field({ - select: Schema.DateTimeUtcFromNumber, + select: Schema.DateTimeUtcFromMillis, insert: DateTimeFromNumberWithNow, update: DateTimeFromNumberWithNow, - json: Schema.DateTimeUtcFromNumber, + json: Schema.DateTimeUtcFromMillis, }) export interface JsonFromString< - S extends Schema.Schema.All | Schema.PropertySignature.All, + S extends Schema.Top, > extends VariantSchema.Field<{ - readonly select: Schema.Schema, string, Schema.Schema.Context> - readonly insert: Schema.Schema, string, Schema.Schema.Context> - readonly update: Schema.Schema, string, Schema.Schema.Context> + readonly select: Schema.fromJsonString + readonly insert: Schema.fromJsonString + readonly update: Schema.fromJsonString readonly json: S readonly jsonCreate: S readonly jsonUpdate: S }> {} /** A JSON value stored as text in the database, object in JSON variants. */ -export const JsonFromString = ( +export const JsonFromString = ( schema: S, ): JsonFromString => { - const parsed = Schema.parseJson(schema as any) + const parsed = Schema.fromJsonString(schema) return Field({ select: parsed, insert: parsed, @@ -362,27 +337,22 @@ export const JsonFromString = extends VariantSchema.Field<{ - readonly select: Schema.brand - readonly insert: VariantSchema.Overrideable, Uint8Array> - readonly update: Schema.brand - readonly json: Schema.brand + readonly select: Schema.brand + readonly insert: VariantSchema.Overrideable> + readonly update: Schema.brand + readonly json: Schema.brand }> {} export const UuidV4WithGenerate = ( - schema: Schema.brand, -): VariantSchema.Overrideable, Uint8Array> => - VariantSchema.Overrideable(Schema.Uint8ArrayFromSelf, schema, { - generate: Option.match({ - onNone: () => Effect.sync(() => crypto.randomUUID()), - onSome: (id) => Effect.succeed(id as any), - }), - decode: Schema.Uint8ArrayFromSelf, - constructorDefault: () => crypto.randomUUID() as any, + schema: Schema.brand, +): VariantSchema.Overrideable> => + VariantSchema.Overrideable(schema, { + defaultValue: Effect.sync(() => crypto.randomUUID() as any), }) /** A UUID v4 field auto-generated on insert. */ export const UuidV4Insert = ( - schema: Schema.brand, + schema: Schema.brand, ): UuidV4Insert => Field({ select: schema, @@ -392,22 +362,19 @@ export const UuidV4Insert = ( }) /** A boolean parsed from 0 or 1. */ -export class BooleanFromNumber extends Schema.transform(Schema.Literals([0, 1]), Schema.Boolean, { - decode: (n) => n === 1, - encode: (b) => (b ? 1 : 0), -}) {} +export const BooleanFromNumber: typeof Schema.BooleanFromBit = Schema.BooleanFromBit -export interface EntitySchema extends Schema.Schema.AnyNoContext { +export interface EntitySchema extends Schema.Top { readonly fields: Schema.Struct.Fields - readonly insert: Schema.Schema.AnyNoContext - readonly update: Schema.Schema.AnyNoContext - readonly json: Schema.Schema.AnyNoContext - readonly jsonCreate: Schema.Schema.AnyNoContext - readonly jsonUpdate: Schema.Schema.AnyNoContext + readonly insert: Schema.Top + readonly update: Schema.Top + readonly json: Schema.Top + readonly jsonCreate: Schema.Top + readonly jsonUpdate: Schema.Top } // Helper utilities for common model fields -export const JsonDate = Schema.Union([Schema.DateFromString, Schema.DateFromSelf]).pipe( +export const JsonDate = Schema.Union([Schema.DateTimeUtcFromString, Schema.Date]).pipe( Schema.annotate({ jsonSchema: { type: "string", format: "date-time" }, }), diff --git a/packages/domain/src/rpc/bots.ts b/packages/domain/src/rpc/bots.ts index 14f5415e5..4378f4cd0 100644 --- a/packages/domain/src/rpc/bots.ts +++ b/packages/domain/src/rpc/bots.ts @@ -113,8 +113,8 @@ export class BotRpcs extends RpcGroup.make( */ Rpc.make("bot.create", { payload: Schema.Struct({ - name: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(100)), - description: Schema.optional(Schema.String.pipe(Schema.maxLength(500))), + name: Schema.String.pipe(Schema.isMinLength(1), Schema.isMaxLength(100)), + description: Schema.optional(Schema.String.pipe(Schema.isMaxLength(500))), webhookUrl: Schema.optional(Schema.String), scopes: Schema.Array(ApiScope), isPublic: Schema.optional(Schema.Boolean), @@ -172,8 +172,8 @@ export class BotRpcs extends RpcGroup.make( Rpc.make("bot.update", { payload: Schema.Struct({ id: BotId, - name: Schema.optional(Schema.String.pipe(Schema.minLength(1), Schema.maxLength(100))), - description: Schema.optional(Schema.NullOr(Schema.String.pipe(Schema.maxLength(500)))), + name: Schema.optional(Schema.String.pipe(Schema.isMinLength(1), Schema.isMaxLength(100))), + description: Schema.optional(Schema.NullOr(Schema.String.pipe(Schema.isMaxLength(500)))), webhookUrl: Schema.optional(Schema.NullOr(Schema.String)), scopes: Schema.optional(Schema.Array(ApiScope)), isPublic: Schema.optional(Schema.Boolean), diff --git a/packages/domain/src/rpc/channel-webhooks.ts b/packages/domain/src/rpc/channel-webhooks.ts index 40109ea95..62ce45798 100644 --- a/packages/domain/src/rpc/channel-webhooks.ts +++ b/packages/domain/src/rpc/channel-webhooks.ts @@ -77,8 +77,8 @@ export class ChannelWebhookRpcs extends RpcGroup.make( Rpc.make("channelWebhook.create", { payload: Schema.Struct({ channelId: ChannelId, - name: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(100)), - description: Schema.optional(Schema.String.pipe(Schema.maxLength(500))), + name: Schema.String.pipe(Schema.isMinLength(1), Schema.isMaxLength(100)), + description: Schema.optional(Schema.String.pipe(Schema.isMaxLength(500))), avatarUrl: Schema.optional(AvatarUrl), /** When set, uses a global integration bot user instead of creating a unique webhook bot */ integrationProvider: Schema.optional(Schema.Literals(["openstatus", "railway"])), @@ -120,8 +120,8 @@ export class ChannelWebhookRpcs extends RpcGroup.make( Rpc.make("channelWebhook.update", { payload: Schema.Struct({ id: ChannelWebhookId, - name: Schema.optional(Schema.String.pipe(Schema.minLength(1), Schema.maxLength(100))), - description: Schema.optional(Schema.NullOr(Schema.String.pipe(Schema.maxLength(500)))), + name: Schema.optional(Schema.String.pipe(Schema.isMinLength(1), Schema.isMaxLength(100))), + description: Schema.optional(Schema.NullOr(Schema.String.pipe(Schema.isMaxLength(500)))), avatarUrl: Schema.optional(Schema.NullOr(AvatarUrl)), isEnabled: Schema.optional(Schema.Boolean), }), diff --git a/packages/integrations/src/discord/api-client.ts b/packages/integrations/src/discord/api-client.ts index 8237c3132..09ed59131 100644 --- a/packages/integrations/src/discord/api-client.ts +++ b/packages/integrations/src/discord/api-client.ts @@ -31,14 +31,14 @@ const DiscordUserApiResponse = Schema.Struct({ id: Schema.String, username: Schema.String, global_name: Schema.optional(Schema.NullOr(Schema.String)), - discriminator: Schema.optionalWith(Schema.String, { default: () => "0" }), + discriminator: Schema.optional(Schema.String, { default: () => "0" }), }) const DiscordGuildApiResponse = Schema.Struct({ id: Schema.String, name: Schema.String, icon: Schema.optional(Schema.NullOr(Schema.String)), - owner: Schema.optionalWith(Schema.Boolean, { default: () => false }), + owner: Schema.optional(Schema.Boolean, { default: () => false }), }) const DiscordWebhookCreateResponse = Schema.Struct({ @@ -59,7 +59,7 @@ const DiscordMessageCreateResponse = Schema.Struct({ }) const DiscordErrorApiResponse = Schema.Struct({ - message: Schema.optionalWith(Schema.String, { default: () => "Unknown Discord error" }), + message: Schema.optional(Schema.String, { default: () => "Unknown Discord error" }), }) export class DiscordApiError extends Schema.TaggedErrorClass()("DiscordApiError", { diff --git a/packages/integrations/src/github/api-client.ts b/packages/integrations/src/github/api-client.ts index 603ff66ba..bbdf6dec9 100644 --- a/packages/integrations/src/github/api-client.ts +++ b/packages/integrations/src/github/api-client.ts @@ -129,16 +129,16 @@ const GitHubPRApiResponse = Schema.Struct({ title: Schema.String, body: Schema.NullOr(Schema.String), state: Schema.String, - draft: Schema.optionalWith(Schema.Boolean, { default: () => false }), - merged: Schema.optionalWith(Schema.Boolean, { default: () => false }), + draft: Schema.optional(Schema.Boolean, { default: () => false }), + merged: Schema.optional(Schema.Boolean, { default: () => false }), user: Schema.NullOr( Schema.Struct({ login: Schema.String, avatar_url: Schema.optional(Schema.NullOr(Schema.String)), }), ), - additions: Schema.optionalWith(Schema.Number, { default: () => 0 }), - deletions: Schema.optionalWith(Schema.Number, { default: () => 0 }), + additions: Schema.optional(Schema.Number, { default: () => 0 }), + deletions: Schema.optional(Schema.Number, { default: () => 0 }), head: Schema.optional(Schema.Struct({ ref: Schema.String })), updated_at: Schema.optional(Schema.String), labels: Schema.optionalWith( @@ -178,13 +178,13 @@ const GitHubRepositoriesApiResponse = Schema.Struct({ // GitHub API error response schema const GitHubErrorApiResponse = Schema.Struct({ - message: Schema.optionalWith(Schema.String, { default: () => "Unknown error" }), + message: Schema.optional(Schema.String, { default: () => "Unknown error" }), }) // GitHub App info response schema const GitHubAppApiResponse = Schema.Struct({ id: Schema.Number, - name: Schema.optionalWith(Schema.String, { default: () => "GitHub App" }), + name: Schema.optional(Schema.String, { default: () => "GitHub App" }), }) // ============================================================================ diff --git a/packages/integrations/src/github/jwt-service.ts b/packages/integrations/src/github/jwt-service.ts index 4be224896..b37d00aee 100644 --- a/packages/integrations/src/github/jwt-service.ts +++ b/packages/integrations/src/github/jwt-service.ts @@ -61,7 +61,7 @@ const InstallationTokenApiResponse = Schema.Struct({ // GitHub API error response schema const GitHubErrorApiResponse = Schema.Struct({ - message: Schema.optionalWith(Schema.String, { default: () => "Unknown error" }), + message: Schema.optional(Schema.String, { default: () => "Unknown error" }), }) // ============================================================================ diff --git a/packages/schema/src/avatar-url.ts b/packages/schema/src/avatar-url.ts index f59f6b985..08656678d 100644 --- a/packages/schema/src/avatar-url.ts +++ b/packages/schema/src/avatar-url.ts @@ -70,7 +70,7 @@ export const AvatarUrl = Schema.String.pipe( Schema.pattern(/^https?:\/\/.+/i, { message: () => "Avatar URL must be a valid URL", }), - Schema.maxLength(2048), + Schema.isMaxLength(2048), Schema.filterEffect((url) => validateImageUrl(url).pipe( Effect.map(() => true), From f48a6e2169293082b9d522490864794438be9fde Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 00:21:16 +0100 Subject: [PATCH 04/34] fix --- .../backend/scripts/rebuild-channel-access.ts | 2 +- apps/backend/scripts/reset-all.ts | 2 +- apps/backend/scripts/setup.ts | 10 +- apps/backend/src/index.ts | 164 +++++++++--------- .../backend/src/policies/attachment-policy.ts | 12 +- apps/backend/src/policies/bot-policy.ts | 4 +- .../src/policies/channel-member-policy.ts | 8 +- apps/backend/src/policies/channel-policy.ts | 4 +- .../src/policies/channel-section-policy.ts | 4 +- .../src/policies/channel-webhook-policy.ts | 6 +- .../src/policies/custom-emoji-policy.ts | 4 +- .../policies/github-subscription-policy.ts | 6 +- .../policies/integration-connection-policy.ts | 2 +- .../backend/src/policies/invitation-policy.ts | 8 +- apps/backend/src/policies/message-policy.ts | 8 +- .../src/policies/message-reaction-policy.ts | 8 +- .../src/policies/notification-policy.ts | 4 +- .../policies/organization-member-policy.ts | 4 +- .../src/policies/organization-policy.ts | 2 +- .../src/policies/pinned-message-policy.ts | 8 +- .../src/policies/rss-subscription-policy.ts | 6 +- .../src/policies/typing-indicator-policy.ts | 4 +- .../src/routes/integration-resources.http.ts | 4 +- apps/backend/src/routes/integrations.http.ts | 2 +- apps/backend/src/scripts/sync-workos.ts | 10 +- .../src/services/bot-gateway-service.ts | 4 +- .../chat-sync-attribution-reconciler.ts | 6 +- .../chat-sync-core-worker.integration.test.ts | 22 +-- .../chat-sync/chat-sync-core-worker.ts | 30 ++-- .../chat-sync/chat-sync-provider-registry.ts | 2 +- .../chat-sync/discord-gateway-service.ts | 4 +- .../chat-sync/discord-sync-worker.test.ts | 4 +- .../services/chat-sync/discord-sync-worker.ts | 2 +- .../services/connect-conversation-service.ts | 16 +- apps/backend/src/services/database.ts | 2 +- .../src/services/integration-token-service.ts | 10 +- .../integrations/integration-bot-service.ts | 8 +- .../integrations/linear-resource-provider.ts | 2 +- .../message-outbox-dispatcher.test.ts | 4 +- .../src/services/message-outbox-dispatcher.ts | 6 +- .../services/message-side-effect-service.ts | 2 +- .../services/oauth/oauth-provider-registry.ts | 4 +- .../src/services/oauth/oauth-provider.ts | 2 +- .../oauth/providers/discord-oauth-provider.ts | 2 +- .../oauth/providers/linear-oauth-provider.ts | 2 +- apps/backend/src/services/org-resolver.ts | 8 +- apps/backend/src/services/rate-limiter.ts | 2 +- apps/backend/src/services/session-manager.ts | 4 +- .../src/services/webhook-bot-service.ts | 4 +- .../src/test/message-outbox-repo.test.ts | 2 +- apps/bot-gateway/src/index.ts | 10 +- apps/cluster/src/index.ts | 12 +- apps/cluster/src/services/bot-user-service.ts | 2 +- .../src/cache/redis-persistence.ts | 2 +- apps/electric-proxy/src/index.ts | 14 +- apps/link-preview-worker/src/index.ts | 2 +- apps/web/src/atoms/desktop-auth.ts | 6 +- apps/web/src/atoms/web-auth.ts | 2 +- apps/web/src/atoms/web-callback-atoms.ts | 4 +- apps/web/src/lib/auth-fetch.ts | 4 +- apps/web/src/lib/auth-token.ts | 6 +- apps/web/src/lib/services/common/runtime.ts | 2 +- .../src/lib/services/desktop/tauri-auth.ts | 4 +- bots/hazel-bot/src/index.ts | 2 +- bots/linear-bot/src/index.ts | 2 +- .../src/hazel-bot-sdk.error-handling.test.ts | 4 +- libs/bot-sdk/src/hazel-bot-sdk.test.ts | 4 +- libs/bot-sdk/src/hazel-bot-sdk.ts | 4 +- libs/bot-sdk/src/rpc/auth-middleware.ts | 2 +- libs/bot-sdk/src/services/health-server.ts | 2 +- libs/bot-sdk/src/streaming/actors-client.ts | 2 +- .../actors/src/auth/config-service.test.ts | 2 +- packages/actors/src/auth/jwks-service.test.ts | 4 +- packages/actors/src/auth/jwks-service.ts | 2 +- .../src/auth/token-validation-service.ts | 6 +- packages/auth/src/consumers/backend-auth.ts | 8 +- packages/auth/src/consumers/proxy-auth.ts | 6 +- packages/auth/src/session/workos-client.ts | 2 +- .../src/repositories/organization-repo.ts | 4 +- .../backend-core/src/services/workos-sync.ts | 10 +- packages/effect-bun/src/Redis.ts | 2 +- .../integrations/src/github/api-client.ts | 2 +- packages/setup/src/index.ts | 10 +- 83 files changed, 300 insertions(+), 300 deletions(-) diff --git a/apps/backend/scripts/rebuild-channel-access.ts b/apps/backend/scripts/rebuild-channel-access.ts index af293218e..d246547e5 100644 --- a/apps/backend/scripts/rebuild-channel-access.ts +++ b/apps/backend/scripts/rebuild-channel-access.ts @@ -46,7 +46,7 @@ const rebuildChannelAccess = Effect.gen(function* () { ) }) -const ChannelAccessSyncLive = ChannelAccessSyncService.Default.pipe(Layer.provideMerge(DatabaseLive)) +const ChannelAccessSyncLive = ChannelAccessSyncService.layer.pipe(Layer.provideMerge(DatabaseLive)) Effect.runPromise( rebuildChannelAccess.pipe( diff --git a/apps/backend/scripts/reset-all.ts b/apps/backend/scripts/reset-all.ts index c724bea2e..93df946e1 100644 --- a/apps/backend/scripts/reset-all.ts +++ b/apps/backend/scripts/reset-all.ts @@ -257,7 +257,7 @@ const resetScript = Effect.gen(function* () { // Run the script with proper Effect runtime const runnable = resetScript.pipe( Effect.provide(DatabaseLive), - Effect.provide(WorkOSClient.Default), + Effect.provide(WorkOSClient.layer), Effect.provide(Logger.minimumLogLevel(LogLevel.Info)), ) diff --git a/apps/backend/scripts/setup.ts b/apps/backend/scripts/setup.ts index a65f5bdb6..3375e4eb0 100644 --- a/apps/backend/scripts/setup.ts +++ b/apps/backend/scripts/setup.ts @@ -110,13 +110,13 @@ const setupScript = Effect.gen(function* () { // Run the script // Build layers with proper dependency wiring const RepoLive = Layer.mergeAll( - UserRepo.Default, - OrganizationRepo.Default, - OrganizationMemberRepo.Default, - InvitationRepo.Default, + UserRepo.layer, + OrganizationRepo.layer, + OrganizationMemberRepo.layer, + InvitationRepo.layer, ).pipe(Layer.provideMerge(DatabaseLive)) -const MainLive = Layer.mergeAll(WorkOSSync.Default, WorkOSClient.Default).pipe( +const MainLive = Layer.mergeAll(WorkOSSync.layer, WorkOSClient.layer).pipe( Layer.provideMerge(RepoLive), Layer.provideMerge(DatabaseLive), ) diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index ce4534b21..74817e3ea 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -126,96 +126,96 @@ const AllRoutes = Layer.mergeAll(HttpApiRoutes, HealthRouter, DocsRoute, RpcRout const TracerLive = createTracingLayer("api") const RepoLive = Layer.mergeAll( - MessageRepo.Default, - ChannelRepo.Default, - ChannelMemberRepo.Default, - ChannelSectionRepo.Default, - ChatSyncConnectionRepo.Default, - ChatSyncChannelLinkRepo.Default, - ChatSyncMessageLinkRepo.Default, - ChatSyncEventReceiptRepo.Default, - ConnectConversationRepo.Default, - ConnectConversationChannelRepo.Default, - ConnectInviteRepo.Default, - ConnectParticipantRepo.Default, - UserRepo.Default, - OrganizationRepo.Default, - OrganizationMemberRepo.Default, - InvitationRepo.Default, - PinnedMessageRepo.Default, - AttachmentRepo.Default, - NotificationRepo.Default, - TypingIndicatorRepo.Default, - MessageReactionRepo.Default, - MessageOutboxRepo.Default, - UserPresenceStatusRepo.Default, - IntegrationConnectionRepo.Default, - IntegrationTokenRepo.Default, - ChannelWebhookRepo.Default, - GitHubSubscriptionRepo.Default, - RssSubscriptionRepo.Default, - BotRepo.Default, - BotCommandRepo.Default, - BotInstallationRepo.Default, - CustomEmojiRepo.Default, + MessageRepo.layer, + ChannelRepo.layer, + ChannelMemberRepo.layer, + ChannelSectionRepo.layer, + ChatSyncConnectionRepo.layer, + ChatSyncChannelLinkRepo.layer, + ChatSyncMessageLinkRepo.layer, + ChatSyncEventReceiptRepo.layer, + ConnectConversationRepo.layer, + ConnectConversationChannelRepo.layer, + ConnectInviteRepo.layer, + ConnectParticipantRepo.layer, + UserRepo.layer, + OrganizationRepo.layer, + OrganizationMemberRepo.layer, + InvitationRepo.layer, + PinnedMessageRepo.layer, + AttachmentRepo.layer, + NotificationRepo.layer, + TypingIndicatorRepo.layer, + MessageReactionRepo.layer, + MessageOutboxRepo.layer, + UserPresenceStatusRepo.layer, + IntegrationConnectionRepo.layer, + IntegrationTokenRepo.layer, + ChannelWebhookRepo.layer, + GitHubSubscriptionRepo.layer, + RssSubscriptionRepo.layer, + BotRepo.layer, + BotCommandRepo.layer, + BotInstallationRepo.layer, + CustomEmojiRepo.layer, ) const PolicyLive = Layer.mergeAll( - OrgResolver.Default, - OrganizationPolicy.Default, - ChannelPolicy.Default, - ChannelSectionPolicy.Default, - MessagePolicy.Default, - InvitationPolicy.Default, - OrganizationMemberPolicy.Default, - ChannelMemberPolicy.Default, - MessageReactionPolicy.Default, - UserPolicy.Default, - AttachmentPolicy.Default, - PinnedMessagePolicy.Default, - TypingIndicatorPolicy.Default, - NotificationPolicy.Default, - UserPresenceStatusPolicy.Default, - IntegrationConnectionPolicy.Default, - ChannelWebhookPolicy.Default, - GitHubSubscriptionPolicy.Default, - RssSubscriptionPolicy.Default, - BotPolicy.Default, - CustomEmojiPolicy.Default, + OrgResolver.layer, + OrganizationPolicy.layer, + ChannelPolicy.layer, + ChannelSectionPolicy.layer, + MessagePolicy.layer, + InvitationPolicy.layer, + OrganizationMemberPolicy.layer, + ChannelMemberPolicy.layer, + MessageReactionPolicy.layer, + UserPolicy.layer, + AttachmentPolicy.layer, + PinnedMessagePolicy.layer, + TypingIndicatorPolicy.layer, + NotificationPolicy.layer, + UserPresenceStatusPolicy.layer, + IntegrationConnectionPolicy.layer, + ChannelWebhookPolicy.layer, + GitHubSubscriptionPolicy.layer, + RssSubscriptionPolicy.layer, + BotPolicy.layer, + CustomEmojiPolicy.layer, ) // ResultPersistence layer for session caching (uses Redis backing) -const PersistenceLive = RedisResultPersistenceLive.pipe(Layer.provide(Redis.Default)) +const PersistenceLive = RedisResultPersistenceLive.pipe(Layer.provide(Redis.layer)) const MainLive = Layer.mergeAll( RepoLive, PolicyLive, - MockDataGenerator.Default, - WorkOSAuth.Default, - WorkOSClient.Default, - WorkOSSync.Default, - WorkOSWebhookVerifier.Default, + MockDataGenerator.layer, + WorkOSAuth.layer, + WorkOSClient.layer, + WorkOSSync.layer, + WorkOSWebhookVerifier.layer, DatabaseLive, - S3.Default, - Redis.Default, + S3.layer, + Redis.layer, PersistenceLive, - GitHub.GitHubAppJWTService.Default, - GitHub.GitHubApiClient.Default, - IntegrationTokenService.Default, - OAuthProviderRegistry.Default, - IntegrationBotService.Default, - ChatSyncAttributionReconciler.Default, - DiscordSyncWorker.Default, - DiscordGatewayService.Default, - MessageSideEffectService.Default, - MessageOutboxDispatcher.Default, - BotGatewayService.Default, - WebhookBotService.Default, - ChannelAccessSyncService.Default, - ConnectConversationService.Default, - RateLimiter.Default, - // SessionManager.Default includes BackendAuth.Default via dependencies - SessionManager.Default, + GitHub.GitHubAppJWTService.layer, + GitHub.GitHubApiClient.layer, + IntegrationTokenService.layer, + OAuthProviderRegistry.layer, + IntegrationBotService.layer, + ChatSyncAttributionReconciler.layer, + DiscordSyncWorker.layer, + DiscordGatewayService.layer, + MessageSideEffectService.layer, + MessageOutboxDispatcher.layer, + BotGatewayService.layer, + WebhookBotService.layer, + ChannelAccessSyncService.layer, + ConnectConversationService.layer, + RateLimiter.layer, + // SessionManager.layer includes BackendAuth.layer via dependencies + SessionManager.layer, ).pipe( Layer.provideMerge(FetchHttpClient.layer), Layer.provideMerge(Layer.setConfigProvider(ConfigProvider.fromEnv())), @@ -229,11 +229,11 @@ const ServerLayer = HttpRouter.serve(AllRoutes).pipe( Layer.provide(TracerLive), Layer.provide( AuthorizationLive.pipe( - // SessionManager.Default includes BackendAuth and UserRepo via dependencies - Layer.provideMerge(SessionManager.Default), - Layer.provideMerge(WorkOSAuth.Default), + // SessionManager.layer includes BackendAuth and UserRepo via dependencies + Layer.provideMerge(SessionManager.layer), + Layer.provideMerge(WorkOSAuth.layer), Layer.provideMerge(PersistenceLive), - Layer.provideMerge(Redis.Default), + Layer.provideMerge(Redis.layer), Layer.provideMerge(DatabaseLive), ), ), diff --git a/apps/backend/src/policies/attachment-policy.ts b/apps/backend/src/policies/attachment-policy.ts index f5823056a..1826c8c79 100644 --- a/apps/backend/src/policies/attachment-policy.ts +++ b/apps/backend/src/policies/attachment-policy.ts @@ -162,11 +162,11 @@ export class AttachmentPolicy extends ServiceMap.Service()("At }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(AttachmentRepo.Default), - Layer.provide(MessageRepo.Default), - Layer.provide(ChannelRepo.Default), - Layer.provide(OrganizationMemberRepo.Default), - Layer.provide(ChannelMemberRepo.Default), - Layer.provide(OrgResolver.Default), + Layer.provide(AttachmentRepo.layer), + Layer.provide(MessageRepo.layer), + Layer.provide(ChannelRepo.layer), + Layer.provide(OrganizationMemberRepo.layer), + Layer.provide(ChannelMemberRepo.layer), + Layer.provide(OrgResolver.layer), ) } diff --git a/apps/backend/src/policies/bot-policy.ts b/apps/backend/src/policies/bot-policy.ts index b09628665..dc05c7a76 100644 --- a/apps/backend/src/policies/bot-policy.ts +++ b/apps/backend/src/policies/bot-policy.ts @@ -111,7 +111,7 @@ export class BotPolicy extends ServiceMap.Service()("BotPolicy/Policy }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(BotRepo.Default), - Layer.provide(OrgResolver.Default), + Layer.provide(BotRepo.layer), + Layer.provide(OrgResolver.layer), ) } diff --git a/apps/backend/src/policies/channel-member-policy.ts b/apps/backend/src/policies/channel-member-policy.ts index 6d0864746..17ff435ed 100644 --- a/apps/backend/src/policies/channel-member-policy.ts +++ b/apps/backend/src/policies/channel-member-policy.ts @@ -166,9 +166,9 @@ export class ChannelMemberPolicy extends ServiceMap.Service }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(ChannelMemberRepo.Default), - Layer.provide(ChannelRepo.Default), - Layer.provide(OrganizationMemberRepo.Default), - Layer.provide(OrgResolver.Default), + Layer.provide(ChannelMemberRepo.layer), + Layer.provide(ChannelRepo.layer), + Layer.provide(OrganizationMemberRepo.layer), + Layer.provide(OrgResolver.layer), ) } diff --git a/apps/backend/src/policies/channel-policy.ts b/apps/backend/src/policies/channel-policy.ts index c9869642a..3dbc5929b 100644 --- a/apps/backend/src/policies/channel-policy.ts +++ b/apps/backend/src/policies/channel-policy.ts @@ -60,7 +60,7 @@ export class ChannelPolicy extends ServiceMap.Service()("ChannelP }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(ChannelRepo.Default), - Layer.provide(OrgResolver.Default), + Layer.provide(ChannelRepo.layer), + Layer.provide(OrgResolver.layer), ) } diff --git a/apps/backend/src/policies/channel-section-policy.ts b/apps/backend/src/policies/channel-section-policy.ts index 43a646399..40bbfd921 100644 --- a/apps/backend/src/policies/channel-section-policy.ts +++ b/apps/backend/src/policies/channel-section-policy.ts @@ -73,7 +73,7 @@ export class ChannelSectionPolicy extends ServiceMap.Service()(" }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(CustomEmojiRepo.Default), - Layer.provide(OrgResolver.Default), + Layer.provide(CustomEmojiRepo.layer), + Layer.provide(OrgResolver.layer), ) } diff --git a/apps/backend/src/policies/github-subscription-policy.ts b/apps/backend/src/policies/github-subscription-policy.ts index 95c8af718..559270732 100644 --- a/apps/backend/src/policies/github-subscription-policy.ts +++ b/apps/backend/src/policies/github-subscription-policy.ts @@ -99,8 +99,8 @@ export class GitHubSubscriptionPolicy extends ServiceMap.Service()("In }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(InvitationRepo.Default), - Layer.provide(OrganizationMemberRepo.Default), - Layer.provide(UserRepo.Default), - Layer.provide(OrgResolver.Default), + Layer.provide(InvitationRepo.layer), + Layer.provide(OrganizationMemberRepo.layer), + Layer.provide(UserRepo.layer), + Layer.provide(OrgResolver.layer), ) } diff --git a/apps/backend/src/policies/message-policy.ts b/apps/backend/src/policies/message-policy.ts index 7a0138d51..51133e0de 100644 --- a/apps/backend/src/policies/message-policy.ts +++ b/apps/backend/src/policies/message-policy.ts @@ -88,9 +88,9 @@ export class MessagePolicy extends ServiceMap.Service()("MessageP }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(MessageRepo.Default), - Layer.provide(ChannelRepo.Default), - Layer.provide(OrganizationMemberRepo.Default), - Layer.provide(OrgResolver.Default), + Layer.provide(MessageRepo.layer), + Layer.provide(ChannelRepo.layer), + Layer.provide(OrganizationMemberRepo.layer), + Layer.provide(OrgResolver.layer), ) } diff --git a/apps/backend/src/policies/message-reaction-policy.ts b/apps/backend/src/policies/message-reaction-policy.ts index 8bf20a314..e047f67e0 100644 --- a/apps/backend/src/policies/message-reaction-policy.ts +++ b/apps/backend/src/policies/message-reaction-policy.ts @@ -102,9 +102,9 @@ export class MessageReactionPolicy extends ServiceMap.Service() }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(NotificationRepo.Default), - Layer.provide(OrganizationMemberRepo.Default), + Layer.provide(NotificationRepo.layer), + Layer.provide(OrganizationMemberRepo.layer), ) } diff --git a/apps/backend/src/policies/organization-member-policy.ts b/apps/backend/src/policies/organization-member-policy.ts index f86f827b2..8baaa2dfc 100644 --- a/apps/backend/src/policies/organization-member-policy.ts +++ b/apps/backend/src/policies/organization-member-policy.ts @@ -106,7 +106,7 @@ export class OrganizationMemberPolicy extends ServiceMap.Service() }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(OrgResolver.Default), + Layer.provide(OrgResolver.layer), ) } diff --git a/apps/backend/src/policies/pinned-message-policy.ts b/apps/backend/src/policies/pinned-message-policy.ts index 6978a1df6..454d2f6f1 100644 --- a/apps/backend/src/policies/pinned-message-policy.ts +++ b/apps/backend/src/policies/pinned-message-policy.ts @@ -114,9 +114,9 @@ export class PinnedMessagePolicy extends ServiceMap.Service }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(PinnedMessageRepo.Default), - Layer.provide(ChannelRepo.Default), - Layer.provide(OrganizationMemberRepo.Default), - Layer.provide(OrgResolver.Default), + Layer.provide(PinnedMessageRepo.layer), + Layer.provide(ChannelRepo.layer), + Layer.provide(OrganizationMemberRepo.layer), + Layer.provide(OrgResolver.layer), ) } diff --git a/apps/backend/src/policies/rss-subscription-policy.ts b/apps/backend/src/policies/rss-subscription-policy.ts index e1fca63ee..62ef7c69d 100644 --- a/apps/backend/src/policies/rss-subscription-policy.ts +++ b/apps/backend/src/policies/rss-subscription-policy.ts @@ -99,8 +99,8 @@ export class RssSubscriptionPolicy extends ServiceMap.Service new IntegrationResourceError({ @@ -469,7 +469,7 @@ const handleGetDiscordGuildChannels = Effect.fn("integration-resources.getDiscor guildId, Redacted.value(botToken), ).pipe( - Effect.provide(Discord.DiscordApiClient.Default), + Effect.provide(Discord.DiscordApiClient.layer), Effect.mapError( (error) => new IntegrationResourceError({ diff --git a/apps/backend/src/routes/integrations.http.ts b/apps/backend/src/routes/integrations.http.ts index 82eede569..578f472e5 100644 --- a/apps/backend/src/routes/integrations.http.ts +++ b/apps/backend/src/routes/integrations.http.ts @@ -1261,4 +1261,4 @@ export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations" ), ), ), -).pipe(Layer.provide(CraftApiClient.Default), Layer.provide(IntegrationBotService.Default)) +).pipe(Layer.provide(CraftApiClient.layer), Layer.provide(IntegrationBotService.layer)) diff --git a/apps/backend/src/scripts/sync-workos.ts b/apps/backend/src/scripts/sync-workos.ts index b95e82c32..216b10af1 100644 --- a/apps/backend/src/scripts/sync-workos.ts +++ b/apps/backend/src/scripts/sync-workos.ts @@ -10,13 +10,13 @@ import { Effect, Layer, Logger } from "effect" import { DatabaseLive } from "../services/database" const RepoLive = Layer.mergeAll( - UserRepo.Default, - OrganizationRepo.Default, - OrganizationMemberRepo.Default, - InvitationRepo.Default, + UserRepo.layer, + OrganizationRepo.layer, + OrganizationMemberRepo.layer, + InvitationRepo.layer, ).pipe(Layer.provideMerge(DatabaseLive)) -const MainLive = Layer.mergeAll(WorkOSSync.Default, WorkOSClient.Default).pipe( +const MainLive = Layer.mergeAll(WorkOSSync.layer, WorkOSClient.layer).pipe( Layer.provideMerge(RepoLive), Layer.provideMerge(DatabaseLive), ) diff --git a/apps/backend/src/services/bot-gateway-service.ts b/apps/backend/src/services/bot-gateway-service.ts index 7ffbadeaf..157c03ffb 100644 --- a/apps/backend/src/services/bot-gateway-service.ts +++ b/apps/backend/src/services/bot-gateway-service.ts @@ -281,7 +281,7 @@ export class BotGatewayService extends ServiceMap.Service()(" }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(BotInstallationRepo.Default), - Layer.provide(ChannelRepo.Default), + Layer.provide(BotInstallationRepo.layer), + Layer.provide(ChannelRepo.layer), ) } diff --git a/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.ts b/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.ts index 79735a61e..a8a320b4a 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.ts @@ -129,8 +129,8 @@ export class ChatSyncAttributionReconciler extends ServiceMap.Service { const repoLayer = Layer.mergeAll( - ChatSyncConnectionRepo.Default, - ChatSyncChannelLinkRepo.Default, - ChatSyncMessageLinkRepo.Default, - ChatSyncEventReceiptRepo.Default, - MessageRepo.Default, - MessageOutboxRepo.Default, - MessageReactionRepo.Default, - ChannelRepo.Default, - IntegrationConnectionRepo.Default, - UserRepo.Default, - OrganizationMemberRepo.Default, + ChatSyncConnectionRepo.layer, + ChatSyncChannelLinkRepo.layer, + ChatSyncMessageLinkRepo.layer, + ChatSyncEventReceiptRepo.layer, + MessageRepo.layer, + MessageOutboxRepo.layer, + MessageReactionRepo.layer, + ChannelRepo.layer, + IntegrationConnectionRepo.layer, + UserRepo.layer, + OrganizationMemberRepo.layer, ).pipe(Layer.provide(harness.dbLayer)) const deps = Layer.mergeAll( diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index 7ee58ba30..8701892e8 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -2762,20 +2762,20 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }), }) { static readonly layer = Layer.effect(this, this.effect).pipe( - Layer.provide(ChatSyncConnectionRepo.Default), - Layer.provide(ChatSyncChannelLinkRepo.Default), - Layer.provide(ChatSyncMessageLinkRepo.Default), - Layer.provide(ChatSyncEventReceiptRepo.Default), - Layer.provide(MessageRepo.Default), - Layer.provide(MessageOutboxRepo.Default), - Layer.provide(MessageReactionRepo.Default), - Layer.provide(ChannelRepo.Default), - Layer.provide(IntegrationConnectionRepo.Default), - Layer.provide(UserRepo.Default), - Layer.provide(OrganizationMemberRepo.Default), - Layer.provide(IntegrationBotService.Default), - Layer.provide(ChannelAccessSyncService.Default), - Layer.provide(ChatSyncProviderRegistry.Default), - Layer.provide(Discord.DiscordApiClient.Default), + Layer.provide(ChatSyncConnectionRepo.layer), + Layer.provide(ChatSyncChannelLinkRepo.layer), + Layer.provide(ChatSyncMessageLinkRepo.layer), + Layer.provide(ChatSyncEventReceiptRepo.layer), + Layer.provide(MessageRepo.layer), + Layer.provide(MessageOutboxRepo.layer), + Layer.provide(MessageReactionRepo.layer), + Layer.provide(ChannelRepo.layer), + Layer.provide(IntegrationConnectionRepo.layer), + Layer.provide(UserRepo.layer), + Layer.provide(OrganizationMemberRepo.layer), + Layer.provide(IntegrationBotService.layer), + Layer.provide(ChannelAccessSyncService.layer), + Layer.provide(ChatSyncProviderRegistry.layer), + Layer.provide(Discord.DiscordApiClient.layer), ) } diff --git a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts index cc84908e7..e745af592 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts @@ -439,6 +439,6 @@ export class ChatSyncProviderRegistry extends ServiceMap.Service ChatSyncProviderRegistry, deps.providerRegistry as ChatSyncProviderRegistry, ) - : ChatSyncProviderRegistry.Default, + : ChatSyncProviderRegistry.layer, ), Layer.provide( deps.discordApiClient @@ -151,7 +151,7 @@ const makeWorkerLayer = (deps: WorkerLayerDeps) => Discord.DiscordApiClient, deps.discordApiClient as Discord.DiscordApiClient, ) - : Discord.DiscordApiClient.Default, + : Discord.DiscordApiClient.layer, ), ), ), diff --git a/apps/backend/src/services/chat-sync/discord-sync-worker.ts b/apps/backend/src/services/chat-sync/discord-sync-worker.ts index 81a0ceb85..8da21fa63 100644 --- a/apps/backend/src/services/chat-sync/discord-sync-worker.ts +++ b/apps/backend/src/services/chat-sync/discord-sync-worker.ts @@ -234,6 +234,6 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(ChatSyncCoreWorker.Default), + Layer.provide(ChatSyncCoreWorker.layer), ) } diff --git a/apps/backend/src/services/connect-conversation-service.ts b/apps/backend/src/services/connect-conversation-service.ts index bd732033f..eb82bf4fd 100644 --- a/apps/backend/src/services/connect-conversation-service.ts +++ b/apps/backend/src/services/connect-conversation-service.ts @@ -323,13 +323,13 @@ export class ConnectConversationService extends ServiceMap.Service new TokenRefreshError({ provider, cause })), ) @@ -441,9 +441,9 @@ export class IntegrationTokenService extends ServiceMap.Service Effect.gen(function* () { const client = yield* Linear.LinearApiClient return yield* client.fetchIssue(issueKey, accessToken) - }).pipe(Effect.provide(Linear.LinearApiClient.Default)) + }).pipe(Effect.provide(Linear.LinearApiClient.layer)) diff --git a/apps/backend/src/services/message-outbox-dispatcher.test.ts b/apps/backend/src/services/message-outbox-dispatcher.test.ts index 7956a7860..ba76cae04 100644 --- a/apps/backend/src/services/message-outbox-dispatcher.test.ts +++ b/apps/backend/src/services/message-outbox-dispatcher.test.ts @@ -33,7 +33,7 @@ type SideEffectOptions = { } const runRepoEffect = (harness: ChatSyncDbHarness, effect: Effect.Effect) => - harness.run(effect.pipe(Effect.provide(MessageOutboxRepo.Default))) + harness.run(effect.pipe(Effect.provide(MessageOutboxRepo.layer))) const runDispatcherEffect = ( harness: ChatSyncDbHarness, @@ -45,7 +45,7 @@ const runDispatcherEffect = ( effect.pipe( Effect.provide(MessageOutboxDispatcher.DefaultWithoutDependencies), Effect.provide(Layer.succeed(MessageSideEffectService, sideEffects)), - Effect.provide(MessageOutboxRepo.Default), + Effect.provide(MessageOutboxRepo.layer), Effect.provide( Layer.succeed(EnvVars, { IS_DEV: true, diff --git a/apps/backend/src/services/message-outbox-dispatcher.ts b/apps/backend/src/services/message-outbox-dispatcher.ts index e28ec4732..f4b748bb0 100644 --- a/apps/backend/src/services/message-outbox-dispatcher.ts +++ b/apps/backend/src/services/message-outbox-dispatcher.ts @@ -236,8 +236,8 @@ export class MessageOutboxDispatcher extends ServiceMap.Service new TokenExchangeError({ diff --git a/apps/backend/src/services/oauth/providers/discord-oauth-provider.ts b/apps/backend/src/services/oauth/providers/discord-oauth-provider.ts index 9c4b4bf29..ed2d7df19 100644 --- a/apps/backend/src/services/oauth/providers/discord-oauth-provider.ts +++ b/apps/backend/src/services/oauth/providers/discord-oauth-provider.ts @@ -19,7 +19,7 @@ export const createDiscordOAuthProvider = (config: OAuthProviderConfig): OAuthPr getAccountInfo: (accessToken: string) => Discord.DiscordApiClient.getAccountInfo(accessToken).pipe( - Effect.provide(Discord.DiscordApiClient.Default), + Effect.provide(Discord.DiscordApiClient.layer), Effect.mapError( (error) => new AccountInfoError({ diff --git a/apps/backend/src/services/oauth/providers/linear-oauth-provider.ts b/apps/backend/src/services/oauth/providers/linear-oauth-provider.ts index f2a777ce2..f41043158 100644 --- a/apps/backend/src/services/oauth/providers/linear-oauth-provider.ts +++ b/apps/backend/src/services/oauth/providers/linear-oauth-provider.ts @@ -34,7 +34,7 @@ export const createLinearOAuthProvider = (config: OAuthProviderConfig): OAuthPro const client = yield* Linear.LinearApiClient return yield* client.getAccountInfo(accessToken) }).pipe( - Effect.provide(Linear.LinearApiClient.Default), + Effect.provide(Linear.LinearApiClient.layer), Effect.mapError( (error) => new AccountInfoError({ diff --git a/apps/backend/src/services/org-resolver.ts b/apps/backend/src/services/org-resolver.ts index 07fb73c9f..870a4cbbc 100644 --- a/apps/backend/src/services/org-resolver.ts +++ b/apps/backend/src/services/org-resolver.ts @@ -249,9 +249,9 @@ export class OrgResolver extends ServiceMap.Service()("OrgResolver" }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(OrganizationMemberRepo.Default), - Layer.provide(ChannelRepo.Default), - Layer.provide(ChannelMemberRepo.Default), - Layer.provide(MessageRepo.Default), + Layer.provide(OrganizationMemberRepo.layer), + Layer.provide(ChannelRepo.layer), + Layer.provide(ChannelMemberRepo.layer), + Layer.provide(MessageRepo.layer), ) } diff --git a/apps/backend/src/services/rate-limiter.ts b/apps/backend/src/services/rate-limiter.ts index a8d3c4d4f..9c36259b1 100644 --- a/apps/backend/src/services/rate-limiter.ts +++ b/apps/backend/src/services/rate-limiter.ts @@ -99,7 +99,7 @@ export class RateLimiter extends ServiceMap.Service()("RateLimiter" }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(Redis.Default), + Layer.provide(Redis.layer), ) } diff --git a/apps/backend/src/services/session-manager.ts b/apps/backend/src/services/session-manager.ts index 7a124ee54..44efc653a 100644 --- a/apps/backend/src/services/session-manager.ts +++ b/apps/backend/src/services/session-manager.ts @@ -38,7 +38,7 @@ export class SessionManager extends ServiceMap.Service()("Sessio }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(BackendAuth.Default), - Layer.provide(UserRepo.Default), + Layer.provide(BackendAuth.layer), + Layer.provide(UserRepo.layer), ) } diff --git a/apps/backend/src/services/webhook-bot-service.ts b/apps/backend/src/services/webhook-bot-service.ts index 2e088d0d3..f7a84e513 100644 --- a/apps/backend/src/services/webhook-bot-service.ts +++ b/apps/backend/src/services/webhook-bot-service.ts @@ -68,7 +68,7 @@ export class WebhookBotService extends ServiceMap.Service()(" }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(UserRepo.Default), - Layer.provide(OrganizationMemberRepo.Default), + Layer.provide(UserRepo.layer), + Layer.provide(OrganizationMemberRepo.layer), ) } diff --git a/apps/backend/src/test/message-outbox-repo.test.ts b/apps/backend/src/test/message-outbox-repo.test.ts index 61a47c67c..358a0db0f 100644 --- a/apps/backend/src/test/message-outbox-repo.test.ts +++ b/apps/backend/src/test/message-outbox-repo.test.ts @@ -21,7 +21,7 @@ const SECOND_AUTHOR_ID = "00000000-0000-0000-0000-000000000110" as UserId const uuid = () => randomUUID() const runRepoEffect = (harness: ChatSyncDbHarness, effect: Effect.Effect) => - harness.run(effect.pipe(Effect.provide(MessageOutboxRepo.Default))) + harness.run(effect.pipe(Effect.provide(MessageOutboxRepo.layer))) describe("MessageOutboxRepo", () => { let harness: ChatSyncDbHarness diff --git a/apps/bot-gateway/src/index.ts b/apps/bot-gateway/src/index.ts index c71083c46..466e669b6 100644 --- a/apps/bot-gateway/src/index.ts +++ b/apps/bot-gateway/src/index.ts @@ -822,7 +822,7 @@ export const instrumentStartupLayer = ( ), ) -export const InstrumentedConfigLive = instrumentStartupLayer(GatewayConfig.Default, { +export const InstrumentedConfigLive = instrumentStartupLayer(GatewayConfig.layer, { dependency: "config", startMessage: "Loading gateway startup config...", successMessage: "Gateway startup config loaded", @@ -847,7 +847,7 @@ export const InstrumentedDatabaseLive = instrumentStartupLayer( }, ) -export const InstrumentedRedisLive = instrumentStartupLayer(Redis.Default, { +export const InstrumentedRedisLive = instrumentStartupLayer(Redis.layer, { dependency: "redis", startMessage: "Initializing bot gateway Redis layer...", successMessage: "Bot gateway Redis layer initialized", @@ -861,9 +861,9 @@ export const InstrumentedTracerLive = instrumentStartupLayer(TracerLive, { failureMessage: "Bot gateway tracer layer initialization failed", }) -const RepoLive = Layer.mergeAll(BotRepo.Default).pipe(Layer.provide(InstrumentedDatabaseLive)) -const DurableStreamClientLive = DurableStreamClient.Default.pipe(Layer.provide(InstrumentedConfigLive)) -const BotGatewayHubLive = BotGatewayHub.Default.pipe( +const RepoLive = Layer.mergeAll(BotRepo.layer).pipe(Layer.provide(InstrumentedDatabaseLive)) +const DurableStreamClientLive = DurableStreamClient.layer.pipe(Layer.provide(InstrumentedConfigLive)) +const BotGatewayHubLive = BotGatewayHub.layer.pipe( Layer.provideMerge(InstrumentedConfigLive), Layer.provideMerge(InstrumentedRedisLive), Layer.provideMerge(RepoLive), diff --git a/apps/cluster/src/index.ts b/apps/cluster/src/index.ts index 2eb58250e..58bff209f 100644 --- a/apps/cluster/src/index.ts +++ b/apps/cluster/src/index.ts @@ -68,12 +68,12 @@ const AllWorkflows = Layer.mergeAll( // WorkOSSync dependencies layer for cron job // Build the layer manually to ensure Database is provided to all deps -const WorkOSSyncLive = WorkOSSync.Default.pipe( - Layer.provide(WorkOSClient.Default), - Layer.provide(UserRepo.Default), - Layer.provide(OrganizationRepo.Default), - Layer.provide(OrganizationMemberRepo.Default), - Layer.provide(InvitationRepo.Default), +const WorkOSSyncLive = WorkOSSync.layer.pipe( + Layer.provide(WorkOSClient.layer), + Layer.provide(UserRepo.layer), + Layer.provide(OrganizationRepo.layer), + Layer.provide(OrganizationMemberRepo.layer), + Layer.provide(InvitationRepo.layer), Layer.provide(DatabaseLayer), ) diff --git a/apps/cluster/src/services/bot-user-service.ts b/apps/cluster/src/services/bot-user-service.ts index 25c1d4341..3a54b11b2 100644 --- a/apps/cluster/src/services/bot-user-service.ts +++ b/apps/cluster/src/services/bot-user-service.ts @@ -123,4 +123,4 @@ export class BotUserService extends ServiceMap.Service()("BotUse /** * Layer that provides BotUserService with Database dependency. */ -export const BotUserServiceLive = BotUserService.Default +export const BotUserServiceLive = BotUserService.layer diff --git a/apps/electric-proxy/src/cache/redis-persistence.ts b/apps/electric-proxy/src/cache/redis-persistence.ts index 07030ab55..5da8b9278 100644 --- a/apps/electric-proxy/src/cache/redis-persistence.ts +++ b/apps/electric-proxy/src/cache/redis-persistence.ts @@ -13,7 +13,7 @@ export const RedisPersistenceLive = Layer.unwrapEffect( yield* Effect.log("Connecting to Redis via @hazel/effect-bun", { url: config.redisUrl }) return RedisResultPersistenceLive.pipe(Layer.provide(Redis.layer(Redacted.value(config.redisUrl)))) }), -).pipe(Layer.provide(ProxyConfigService.Default)) +).pipe(Layer.provide(ProxyConfigService.layer)) /** * In-memory persistence layer for testing or fallback. diff --git a/apps/electric-proxy/src/index.ts b/apps/electric-proxy/src/index.ts index a27841ed3..a8cf57eee 100644 --- a/apps/electric-proxy/src/index.ts +++ b/apps/electric-proxy/src/index.ts @@ -435,25 +435,25 @@ const LoggerLive = Layer.unwrapEffect( const config = yield* ProxyConfigService return config.isDev ? Logger.pretty : Logger.structured }), -).pipe(Layer.provide(ProxyConfigService.Default)) +).pipe(Layer.provide(ProxyConfigService.layer)) // Cache layer: AccessContextCache requires ResultPersistence and Database -const CacheLive = AccessContextCache.Default.pipe( +const CacheLive = AccessContextCache.layer.pipe( Layer.provide(RedisPersistenceLive), Layer.provide(DatabaseLive), - Layer.provide(ProxyConfigService.Default), + Layer.provide(ProxyConfigService.layer), ) // ProxyAuth layer requires ResultPersistence for session caching and Database for user lookup -// ProxyAuth.Default includes SessionValidator.Default via dependencies -const ProxyAuthLive = ProxyAuth.Default.pipe( +// ProxyAuth.layer includes SessionValidator.layer via dependencies +const ProxyAuthLive = ProxyAuth.layer.pipe( Layer.provide(RedisPersistenceLive), Layer.provide(DatabaseLive), - Layer.provide(ProxyConfigService.Default), + Layer.provide(ProxyConfigService.layer), ) const MainLive = DatabaseLive.pipe( - Layer.provideMerge(ProxyConfigService.Default), + Layer.provideMerge(ProxyConfigService.layer), Layer.provideMerge(LoggerLive), Layer.provideMerge(CacheLive), Layer.provideMerge(TracerLive), diff --git a/apps/link-preview-worker/src/index.ts b/apps/link-preview-worker/src/index.ts index aa0be5b05..3ae319ceb 100644 --- a/apps/link-preview-worker/src/index.ts +++ b/apps/link-preview-worker/src/index.ts @@ -22,7 +22,7 @@ const makeHttpLiveWithKV = (env: Env) => ), Layer.provideMerge(HttpServer.layerContext), Layer.provide(makeKVCacheLayer(env.LINK_CACHE)), - Layer.provide(TwitterApi.Default), + Layer.provide(TwitterApi.layer), Layer.provide(Logger.pretty), ) diff --git a/apps/web/src/atoms/desktop-auth.ts b/apps/web/src/atoms/desktop-auth.ts index 6d11a6991..fe513d470 100644 --- a/apps/web/src/atoms/desktop-auth.ts +++ b/apps/web/src/atoms/desktop-auth.ts @@ -56,9 +56,9 @@ const REFRESH_BUFFER_MS = 5 * 60 * 1000 // Layers // ============================================================================ -const TokenStorageLive = TokenStorage.Default -const TokenExchangeLive = TokenExchange.Default -const TauriAuthLive = TauriAuth.Default +const TokenStorageLive = TokenStorage.layer +const TokenExchangeLive = TokenExchange.layer +const TauriAuthLive = TauriAuth.layer const ClipboardLive = Clipboard.layer // ============================================================================ diff --git a/apps/web/src/atoms/web-auth.ts b/apps/web/src/atoms/web-auth.ts index be71b4dd9..d3f989ddf 100644 --- a/apps/web/src/atoms/web-auth.ts +++ b/apps/web/src/atoms/web-auth.ts @@ -79,7 +79,7 @@ const REFRESH_BUFFER_MS = 5 * 60 * 1000 // Layers // ============================================================================ -const WebTokenStorageLive = WebTokenStorage.Default +const WebTokenStorageLive = WebTokenStorage.layer // ============================================================================ // Core State Atoms diff --git a/apps/web/src/atoms/web-callback-atoms.ts b/apps/web/src/atoms/web-callback-atoms.ts index 32a473b9b..90bfc6e70 100644 --- a/apps/web/src/atoms/web-callback-atoms.ts +++ b/apps/web/src/atoms/web-callback-atoms.ts @@ -62,8 +62,8 @@ export const webCallbackStatusAtom = Atom.make({ _tag: "idle" // Layers // ============================================================================ -const WebTokenStorageLive = WebTokenStorage.Default -const TokenExchangeLive = TokenExchange.Default +const WebTokenStorageLive = WebTokenStorage.layer +const TokenExchangeLive = TokenExchange.layer // ============================================================================ // Error Handling diff --git a/apps/web/src/lib/auth-fetch.ts b/apps/web/src/lib/auth-fetch.ts index 93379980c..31ef13db8 100644 --- a/apps/web/src/lib/auth-fetch.ts +++ b/apps/web/src/lib/auth-fetch.ts @@ -15,8 +15,8 @@ import { WebTokenStorage } from "./services/web/token-storage" import { runtime } from "./services/common/runtime" import { isTauri } from "./tauri" -const DesktopTokenStorageLive = TokenStorage.Default -const WebTokenStorageLive = WebTokenStorage.Default +const DesktopTokenStorageLive = TokenStorage.layer +const WebTokenStorageLive = WebTokenStorage.layer /** * Clear tokens from appropriate storage (desktop or web) diff --git a/apps/web/src/lib/auth-token.ts b/apps/web/src/lib/auth-token.ts index 83f985757..25e2b757b 100644 --- a/apps/web/src/lib/auth-token.ts +++ b/apps/web/src/lib/auth-token.ts @@ -33,9 +33,9 @@ const refreshDeferredRef = Ref.unsafeMake | null>(nul // Platform-specific layers // ============================================================================ -const webStorageLive = WebTokenStorage.Default -const desktopStorageLive = TokenStorage.Default -const tokenExchangeLive = TokenExchange.Default +const webStorageLive = WebTokenStorage.layer +const desktopStorageLive = TokenStorage.layer +const tokenExchangeLive = TokenExchange.layer // ============================================================================ // Error Classification diff --git a/apps/web/src/lib/services/common/runtime.ts b/apps/web/src/lib/services/common/runtime.ts index fba8f719c..c4e8c527f 100644 --- a/apps/web/src/lib/services/common/runtime.ts +++ b/apps/web/src/lib/services/common/runtime.ts @@ -15,7 +15,7 @@ import { TracerLive } from "./telemetry" * * All RPC clients share the same WebSocket connection via RpcProtocolLive. */ -export const runtimeLayer = Layer.mergeAll(ApiClient.Default, HazelRpcClient.layer, TracerLive) +export const runtimeLayer = Layer.mergeAll(ApiClient.layer, HazelRpcClient.layer, TracerLive) /** * Managed runtime for imperative Effect execution diff --git a/apps/web/src/lib/services/desktop/tauri-auth.ts b/apps/web/src/lib/services/desktop/tauri-auth.ts index 38cc9ad71..5385590d0 100644 --- a/apps/web/src/lib/services/desktop/tauri-auth.ts +++ b/apps/web/src/lib/services/desktop/tauri-auth.ts @@ -206,7 +206,7 @@ export class TauriAuth extends ServiceMap.Service()("TauriAuth", { }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(TokenStorage.Default), - Layer.provide(TokenExchange.Default), + Layer.provide(TokenStorage.layer), + Layer.provide(TokenExchange.layer), ) } diff --git a/bots/hazel-bot/src/index.ts b/bots/hazel-bot/src/index.ts index 17931582f..06b937cfb 100644 --- a/bots/hazel-bot/src/index.ts +++ b/bots/hazel-bot/src/index.ts @@ -24,7 +24,7 @@ const bot = defineBot({ serviceName: "hazel-bot", commands, mentionable: true, - layers: [LinearApiClient.Default, CraftApiClient.Default], + layers: [LinearApiClient.layer, CraftApiClient.layer], setup: (bot) => Effect.gen(function* () { const loadActiveThreads = () => diff --git a/bots/linear-bot/src/index.ts b/bots/linear-bot/src/index.ts index 6a57de2e2..b7d84076a 100644 --- a/bots/linear-bot/src/index.ts +++ b/bots/linear-bot/src/index.ts @@ -25,7 +25,7 @@ const OpenRouterModelLayer = OpenRouterLanguageModel.layer({ runHazelBot({ serviceName: "linear-bot", commands, - layers: [LinearApiClient.Default], + layers: [LinearApiClient.layer], setup: (bot) => Effect.gen(function* () { yield* bot.onCommand(IssueCommand, (ctx) => diff --git a/libs/bot-sdk/src/hazel-bot-sdk.error-handling.test.ts b/libs/bot-sdk/src/hazel-bot-sdk.error-handling.test.ts index 954048f27..53883405d 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.error-handling.test.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.error-handling.test.ts @@ -52,9 +52,9 @@ const makeMessageResponse = (content: string) => ({ }) const makeHazelBotLayer = () => - HazelBotClient.Default.pipe( + HazelBotClient.layer.pipe( Layer.provide( - BotAuth.Default({ + BotAuth.layer({ botId: BOT_ID, botName: "Test Bot", userId: USER_ID, diff --git a/libs/bot-sdk/src/hazel-bot-sdk.test.ts b/libs/bot-sdk/src/hazel-bot-sdk.test.ts index c5269da91..c815c8485 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.test.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.test.ts @@ -122,9 +122,9 @@ const makeHazelBotLayer = (options: { delete: (botId: string, key: string) => Effect.Effect } }) => { - return HazelBotClient.Default.pipe( + return HazelBotClient.layer.pipe( Layer.provide( - BotAuth.Default({ + BotAuth.layer({ botId: BOT_ID, botName: "Test Bot", userId: USER_ID, diff --git a/libs/bot-sdk/src/hazel-bot-sdk.ts b/libs/bot-sdk/src/hazel-bot-sdk.ts index 9229d3b4e..ffb4bb7c7 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.ts @@ -1957,7 +1957,7 @@ export const createHazelBot = = EmptyCommand const AuthLayer = Layer.unwrapEffect( createAuthContextFromToken(config.botToken, backendUrl).pipe( - Effect.map((context) => BotAuth.Default(context)), + Effect.map((context) => BotAuth.layer(context)), ), ) @@ -2023,7 +2023,7 @@ export const createHazelBot = = EmptyCommand // Compose all layers with proper dependency order const AllLayers = Layer.mergeAll( - HazelBotClient.Default.pipe( + HazelBotClient.layer.pipe( Layer.provide(RpcClientLayer), Layer.provide(RpcClientConfigLayer), Layer.provide(BotStateStoreLayer), diff --git a/libs/bot-sdk/src/rpc/auth-middleware.ts b/libs/bot-sdk/src/rpc/auth-middleware.ts index a34a99112..e27a4f53a 100644 --- a/libs/bot-sdk/src/rpc/auth-middleware.ts +++ b/libs/bot-sdk/src/rpc/auth-middleware.ts @@ -21,7 +21,7 @@ import { Effect } from "effect" * ```typescript * const BotAuthMiddlewareLive = createBotAuthMiddleware(config.botToken) * - * const RpcClientLayer = BotRpcClient.Default.pipe( + * const RpcClientLayer = BotRpcClient.layer.pipe( * Layer.provide(RpcProtocolLive), * Layer.provide(BotAuthMiddlewareLive), * ) diff --git a/libs/bot-sdk/src/services/health-server.ts b/libs/bot-sdk/src/services/health-server.ts index 18ab22734..403e4bec5 100644 --- a/libs/bot-sdk/src/services/health-server.ts +++ b/libs/bot-sdk/src/services/health-server.ts @@ -77,4 +77,4 @@ export class BotHealthServer extends ServiceMap.Service()("BotH } export const BotHealthServerLive = (port: number) => - Layer.provide(BotHealthServer.Default, Layer.succeed(BotHealthServerConfigTag, { port })) + Layer.provide(BotHealthServer.layer, Layer.succeed(BotHealthServerConfigTag, { port })) diff --git a/libs/bot-sdk/src/streaming/actors-client.ts b/libs/bot-sdk/src/streaming/actors-client.ts index eed7fb2f7..3de6d7d80 100644 --- a/libs/bot-sdk/src/streaming/actors-client.ts +++ b/libs/bot-sdk/src/streaming/actors-client.ts @@ -60,7 +60,7 @@ export interface ActorsClientConfig { * @example * ```typescript * // Create layer with config - * const layer = ActorsClient.Default({ + * const layer = ActorsClient.layer({ * botToken: "hzl_bot_xxx", * endpoint: "http://localhost:6420" * }) diff --git a/packages/actors/src/auth/config-service.test.ts b/packages/actors/src/auth/config-service.test.ts index 8aacbc9c8..67ce0869e 100644 --- a/packages/actors/src/auth/config-service.test.ts +++ b/packages/actors/src/auth/config-service.test.ts @@ -22,7 +22,7 @@ const resetEnv = () => { const loadConfig = Effect.gen(function* () { return yield* TokenValidationConfigService -}).pipe(Effect.provide(TokenValidationConfigService.Default)) +}).pipe(Effect.provide(TokenValidationConfigService.layer)) afterEach(() => { resetEnv() diff --git a/packages/actors/src/auth/jwks-service.test.ts b/packages/actors/src/auth/jwks-service.test.ts index ac6ef424e..dbb6bc9c1 100644 --- a/packages/actors/src/auth/jwks-service.test.ts +++ b/packages/actors/src/auth/jwks-service.test.ts @@ -32,7 +32,7 @@ describe("JwksService", () => { Effect.gen(function* () { const service = yield* JwksService return yield* service.getJwks() - }).pipe(Effect.provide(JwksService.Default), Effect.either), + }).pipe(Effect.provide(JwksService.layer), Effect.either), ) expect(Either.isLeft(result)).toBe(true) @@ -50,7 +50,7 @@ describe("JwksService", () => { const firstJwks = yield* service.getJwks() const secondJwks = yield* service.getJwks() return [firstJwks, secondJwks] as const - }).pipe(Effect.provide(JwksService.Default)), + }).pipe(Effect.provide(JwksService.layer)), ) expect(typeof first).toBe("function") diff --git a/packages/actors/src/auth/jwks-service.ts b/packages/actors/src/auth/jwks-service.ts index a8895756f..3fc99257e 100644 --- a/packages/actors/src/auth/jwks-service.ts +++ b/packages/actors/src/auth/jwks-service.ts @@ -41,6 +41,6 @@ export class JwksService extends ServiceMap.Service()("JwksService" }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(TokenValidationConfigService.Default), + Layer.provide(TokenValidationConfigService.layer), ) } diff --git a/packages/actors/src/auth/token-validation-service.ts b/packages/actors/src/auth/token-validation-service.ts index 7d806c7fb..59286783d 100644 --- a/packages/actors/src/auth/token-validation-service.ts +++ b/packages/actors/src/auth/token-validation-service.ts @@ -243,8 +243,8 @@ export class TokenValidationService extends ServiceMap.Service()("@hazel/auth/ }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(WorkOSClient.Default), + Layer.provide(WorkOSClient.layer), ) /** Mock user ID - a valid UUID */ @@ -426,7 +426,7 @@ export class BackendAuth extends ServiceMap.Service()("@hazel/auth/ /** * Layer that provides BackendAuth with all its dependencies. * - * With Effect.Service dependencies, BackendAuth.Default automatically includes: - * - WorkOSClient.Default (which includes AuthConfig.Default) + * With Effect.Service dependencies, BackendAuth.layer automatically includes: + * - WorkOSClient.layer (which includes AuthConfig.layer) */ -export const BackendAuthLive = BackendAuth.Default +export const BackendAuthLive = BackendAuth.layer diff --git a/packages/auth/src/consumers/proxy-auth.ts b/packages/auth/src/consumers/proxy-auth.ts index 425f10efa..9b76c49af 100644 --- a/packages/auth/src/consumers/proxy-auth.ts +++ b/packages/auth/src/consumers/proxy-auth.ts @@ -206,8 +206,8 @@ export class ProxyAuth extends ServiceMap.Service()("@hazel/auth/Prox }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(UserLookupCache.Default), - Layer.provide(WorkOSClient.Default), + Layer.provide(UserLookupCache.layer), + Layer.provide(WorkOSClient.layer), ) } @@ -217,4 +217,4 @@ export class ProxyAuth extends ServiceMap.Service()("@hazel/auth/Prox * External dependencies that must be provided: * - Database.Database (for user lookup) */ -export const ProxyAuthLive = ProxyAuth.Default +export const ProxyAuthLive = ProxyAuth.layer diff --git a/packages/auth/src/session/workos-client.ts b/packages/auth/src/session/workos-client.ts index 6325e1784..1c5ecaddc 100644 --- a/packages/auth/src/session/workos-client.ts +++ b/packages/auth/src/session/workos-client.ts @@ -55,7 +55,7 @@ export class WorkOSClient extends ServiceMap.Service()("@hazel/aut }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(AuthConfig.Default), + Layer.provide(AuthConfig.layer), ) /** Default mock user for tests */ diff --git a/packages/backend-core/src/repositories/organization-repo.ts b/packages/backend-core/src/repositories/organization-repo.ts index 2a5e6dc11..d986a52cc 100644 --- a/packages/backend-core/src/repositories/organization-repo.ts +++ b/packages/backend-core/src/repositories/organization-repo.ts @@ -118,7 +118,7 @@ export class OrganizationRepo extends ServiceMap.Service()("Or }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(ChannelRepo.Default), - Layer.provide(ChannelMemberRepo.Default), + Layer.provide(ChannelRepo.layer), + Layer.provide(ChannelMemberRepo.layer), ) } diff --git a/packages/backend-core/src/services/workos-sync.ts b/packages/backend-core/src/services/workos-sync.ts index 64547283d..0b3a3bbdf 100644 --- a/packages/backend-core/src/services/workos-sync.ts +++ b/packages/backend-core/src/services/workos-sync.ts @@ -993,10 +993,10 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { }), }) { static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(WorkOSClient.Default), - Layer.provide(UserRepo.Default), - Layer.provide(OrganizationRepo.Default), - Layer.provide(OrganizationMemberRepo.Default), - Layer.provide(InvitationRepo.Default), + Layer.provide(WorkOSClient.layer), + Layer.provide(UserRepo.layer), + Layer.provide(OrganizationRepo.layer), + Layer.provide(OrganizationMemberRepo.layer), + Layer.provide(InvitationRepo.layer), ) } diff --git a/packages/effect-bun/src/Redis.ts b/packages/effect-bun/src/Redis.ts index 44045f87c..39b39bc91 100644 --- a/packages/effect-bun/src/Redis.ts +++ b/packages/effect-bun/src/Redis.ts @@ -89,7 +89,7 @@ const sanitizeRedisUrl = (url: string): string => url.replace(/\/\/.*@/, "//***@ * yield* redis.set("key", "value") * const value = yield* redis.get("key") * return value - * }).pipe(Effect.provide(Redis.Default)) + * }).pipe(Effect.provide(Redis.layer)) * ``` */ export class Redis extends ServiceMap.Service Date: Mon, 16 Mar 2026 00:30:24 +0100 Subject: [PATCH 05/34] fix --- packages/domain/src/cluster/api.ts | 2 +- packages/domain/src/http/api-v1/messages.ts | 93 +++++----- packages/domain/src/http/api.ts | 4 +- packages/domain/src/http/auth.ts | 122 ++++++------ packages/domain/src/http/bot-commands.ts | 140 +++++++------- packages/domain/src/http/chat-sync.ts | 109 ++++++----- packages/domain/src/http/incoming-webhooks.ts | 78 ++++---- .../domain/src/http/integration-commands.ts | 18 +- .../domain/src/http/integration-resources.ts | 127 +++++-------- packages/domain/src/http/integrations.ts | 173 ++++++++---------- packages/domain/src/http/internal.ts | 15 +- packages/domain/src/http/klipy.ts | 43 +++-- packages/domain/src/http/mock-data.ts | 14 +- packages/domain/src/http/presence.ts | 13 +- packages/domain/src/http/root.ts | 2 +- packages/domain/src/http/uploads.ts | 77 ++++---- packages/domain/src/http/webhooks.ts | 29 ++- packages/effect-bun/src/Telemetry.ts | 12 +- .../src/persistence/redis-backing.ts | 24 +-- 19 files changed, 494 insertions(+), 601 deletions(-) diff --git a/packages/domain/src/cluster/api.ts b/packages/domain/src/cluster/api.ts index 87fe8d7b5..09145c69e 100644 --- a/packages/domain/src/cluster/api.ts +++ b/packages/domain/src/cluster/api.ts @@ -23,4 +23,4 @@ export const workflows = [ // HTTP API definition for the cluster service export class WorkflowApi extends HttpApi.make("api") .add(WorkflowProxy.toHttpApiGroup("workflows", workflows)) - .add(HttpApiGroup.make("health").add(HttpApiEndpoint.get("ok")`/health`.addSuccess(Schema.String))) {} + .add(HttpApiGroup.make("health").add(HttpApiEndpoint.get("ok", "/health", { success: Schema.String }))) {} diff --git a/packages/domain/src/http/api-v1/messages.ts b/packages/domain/src/http/api-v1/messages.ts index 552d38bfc..3026cdc9c 100644 --- a/packages/domain/src/http/api-v1/messages.ts +++ b/packages/domain/src/http/api-v1/messages.ts @@ -18,8 +18,8 @@ export class ListMessagesQuery extends Schema.Class("ListMess ending_before: Schema.optional(MessageId), /** Maximum number of messages to return (1-100, default 25) */ limit: Schema.optional( - Schema.NumberFromString.pipe( - Schema.int(), + Schema.NumberFromString.check( + Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(100), ), @@ -27,7 +27,7 @@ export class ListMessagesQuery extends Schema.Class("ListMess }) {} export class ListMessagesResponse extends Schema.Class("ListMessagesResponse")({ - data: Schema.Array(Message.Model.json), + data: Schema.Array(Message.Model.json as any), has_more: Schema.Boolean, }) {} @@ -55,7 +55,7 @@ export class ToggleReactionRequest extends Schema.Class(" // ============ RESPONSE SCHEMAS ============ export class MessageResponse extends Schema.Class("MessageResponse")({ - data: Message.Model.json, + data: Message.Model.json as any, transactionId: TransactionId, }) {} @@ -65,7 +65,7 @@ export class DeleteMessageResponse extends Schema.Class(" export class ToggleReactionResponse extends Schema.Class("ToggleReactionResponse")({ wasCreated: Schema.Boolean, - data: Schema.optional(MessageReaction.Model.json), + data: Schema.optional(MessageReaction.Model.json as any), transactionId: TransactionId, }) {} @@ -92,15 +92,13 @@ export class InvalidPaginationError extends Schema.TaggedErrorClass("DesktopAut export class AuthGroup extends HttpApiGroup.make("auth") .add( - HttpApiEndpoint.get("login")`/login` - .addSuccess(LoginResponse) - .addError(InternalServerError) - .setUrlParams( - Schema.Struct({ - returnTo: Schema.String, - organizationId: Schema.optional(OrganizationId), - invitationToken: Schema.optional(Schema.String), - }), - ) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.get("login", "/login", { + query: { + returnTo: Schema.String, + organizationId: Schema.optional(OrganizationId), + invitationToken: Schema.optional(Schema.String), + }, + success: LoginResponse, + error: InternalServerError, + }) + .annotateMerge( + OpenApi.annotations({ title: "Login", description: "Get WorkOS authorization URL for authentication", summary: "Initiate login flow", @@ -78,18 +77,16 @@ export class AuthGroup extends HttpApiGroup.make("auth") .annotate(RequiredScopes, []), ) .add( - HttpApiEndpoint.get("callback")`/callback` - .addSuccess(Schema.Void, { status: 302 }) - .addError(UnauthorizedError) - .addError(InternalServerError) - .setUrlParams( - Schema.Struct({ - code: Schema.String, - state: Schema.String, - }), - ) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.get("callback", "/callback", { + query: { + code: Schema.String, + state: Schema.String, + }, + success: Schema.Void.pipe(HttpApiSchema.status(302)), + error: [UnauthorizedError, InternalServerError], + }) + .annotateMerge( + OpenApi.annotations({ title: "OAuth Callback", description: "Handle OAuth callback from WorkOS and set session cookie", summary: "Process OAuth callback", @@ -98,16 +95,15 @@ export class AuthGroup extends HttpApiGroup.make("auth") .annotate(RequiredScopes, []), ) .add( - HttpApiEndpoint.get("logout")`/logout` - .addSuccess(Schema.Void) - .addError(InternalServerError) - .setUrlParams( - Schema.Struct({ - redirectTo: Schema.optional(Schema.String), - }), - ) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.get("logout", "/logout", { + query: { + redirectTo: Schema.optional(Schema.String), + }, + success: Schema.Void, + error: InternalServerError, + }) + .annotateMerge( + OpenApi.annotations({ title: "Logout", description: "Clear session and logout user", summary: "End user session", @@ -116,20 +112,19 @@ export class AuthGroup extends HttpApiGroup.make("auth") .annotate(RequiredScopes, []), ) .add( - HttpApiEndpoint.get("loginDesktop")`/login/desktop` - .addSuccess(Schema.Void, { status: 302 }) - .addError(InternalServerError) - .setUrlParams( - Schema.Struct({ - returnTo: Schema.String, - desktopPort: Schema.NumberFromString, - desktopNonce: Schema.String, - organizationId: Schema.optional(OrganizationId), - invitationToken: Schema.optional(Schema.String), - }), - ) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.get("loginDesktop", "/login/desktop", { + query: { + returnTo: Schema.String, + desktopPort: Schema.NumberFromString, + desktopNonce: Schema.String, + organizationId: Schema.optional(OrganizationId), + invitationToken: Schema.optional(Schema.String), + }, + success: Schema.Void.pipe(HttpApiSchema.status(302)), + error: InternalServerError, + }) + .annotateMerge( + OpenApi.annotations({ title: "Desktop Login", description: "Initiate OAuth flow for desktop apps with web callback", summary: "Desktop login flow", @@ -138,14 +133,13 @@ export class AuthGroup extends HttpApiGroup.make("auth") .annotate(RequiredScopes, []), ) .add( - HttpApiEndpoint.post("token")`/token` - .addSuccess(TokenResponse) - .addError(UnauthorizedError) - .addError(OAuthCodeExpiredError) - .addError(InternalServerError) - .setPayload(TokenRequest) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.post("token", "/token", { + payload: TokenRequest, + success: TokenResponse, + error: [UnauthorizedError, OAuthCodeExpiredError, InternalServerError], + }) + .annotateMerge( + OpenApi.annotations({ title: "Token Exchange", description: "Exchange authorization code for access token (desktop apps)", summary: "Exchange code for token", @@ -154,13 +148,13 @@ export class AuthGroup extends HttpApiGroup.make("auth") .annotate(RequiredScopes, []), ) .add( - HttpApiEndpoint.post("refresh")`/refresh` - .addSuccess(RefreshTokenResponse) - .addError(UnauthorizedError) - .addError(InternalServerError) - .setPayload(RefreshTokenRequest) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.post("refresh", "/refresh", { + payload: RefreshTokenRequest, + success: RefreshTokenResponse, + error: [UnauthorizedError, InternalServerError], + }) + .annotateMerge( + OpenApi.annotations({ title: "Refresh Token", description: "Exchange refresh token for new access token (desktop apps)", summary: "Refresh access token", diff --git a/packages/domain/src/http/bot-commands.ts b/packages/domain/src/http/bot-commands.ts index e11854180..1d61853c8 100644 --- a/packages/domain/src/http/bot-commands.ts +++ b/packages/domain/src/http/bot-commands.ts @@ -99,7 +99,7 @@ export class IntegrationTokenResponse extends Schema.Class( @@ -134,10 +134,11 @@ export class BotCommandsApiGroup extends HttpApiGroup.make("bot-commands") // SSE stream for bot commands (bot token auth) // This endpoint uses bot token authentication, not user auth .add( - HttpApiEndpoint.get("streamCommands", `/stream`) - .addError(UnauthorizedError) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.get("streamCommands", `/stream`, { + error: UnauthorizedError, + }) + .annotateMerge( + OpenApi.annotations({ title: "Stream Bot Commands", description: "SSE stream for receiving bot commands (used by Bot SDK)", summary: "Stream commands via SSE", @@ -148,11 +149,12 @@ export class BotCommandsApiGroup extends HttpApiGroup.make("bot-commands") // Get current bot info (for bot token validation) // This endpoint uses bot token authentication, not user auth .add( - HttpApiEndpoint.get("getBotMe", `/me`) - .addSuccess(BotMeResponse) - .addError(UnauthorizedError) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.get("getBotMe", `/me`, { + success: BotMeResponse, + error: UnauthorizedError, + }) + .annotateMerge( + OpenApi.annotations({ title: "Get Bot Info", description: "Get current bot info from token (used by Bot SDK for authentication)", summary: "Get bot info", @@ -163,14 +165,13 @@ export class BotCommandsApiGroup extends HttpApiGroup.make("bot-commands") // Sync commands from bot (called by Bot SDK on startup) // This endpoint uses bot token authentication, not user auth .add( - HttpApiEndpoint.post("syncCommands", `/sync`) - .addSuccess(SyncBotCommandsResponse) - .addError(BotNotFoundError) - .addError(UnauthorizedError) - .addError(InternalServerError) - .setPayload(SyncBotCommandsRequest) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.post("syncCommands", `/sync`, { + payload: SyncBotCommandsRequest, + success: SyncBotCommandsResponse, + error: [BotNotFoundError, UnauthorizedError, InternalServerError], + }) + .annotateMerge( + OpenApi.annotations({ title: "Sync Bot Commands", description: "Sync slash commands from a bot (called by Bot SDK on startup)", summary: "Register bot commands", @@ -180,24 +181,25 @@ export class BotCommandsApiGroup extends HttpApiGroup.make("bot-commands") ) // Execute a bot command (frontend calls this - requires user auth) .add( - HttpApiEndpoint.post("executeBotCommand", `/:orgId/bots/:botId/commands/:commandName/execute`) - .addSuccess(BotCommandExecutionAccepted) - .addError(BotNotFoundError) - .addError(BotNotInstalledError) - .addError(BotCommandNotFoundError) - .addError(BotCommandExecutionError) - .addError(UnauthorizedError) - .addError(InternalServerError) - .setPath( - Schema.Struct({ - orgId: OrganizationId, - botId: BotId, - commandName: Schema.String, - }), - ) - .setPayload(ExecuteBotCommandRequest) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.post("executeBotCommand", `/:orgId/bots/:botId/commands/:commandName/execute`, { + params: { + orgId: OrganizationId, + botId: BotId, + commandName: Schema.String, + }, + payload: ExecuteBotCommandRequest, + success: BotCommandExecutionAccepted, + error: [ + BotNotFoundError, + BotNotInstalledError, + BotCommandNotFoundError, + BotCommandExecutionError, + UnauthorizedError, + InternalServerError, + ], + }) + .annotateMerge( + OpenApi.annotations({ title: "Execute Bot Command", description: "Execute a slash command for a bot", summary: "Execute bot command", @@ -209,21 +211,22 @@ export class BotCommandsApiGroup extends HttpApiGroup.make("bot-commands") // Get integration token (bot token auth) // Bot must have the provider in its allowedIntegrations and be installed in the org .add( - HttpApiEndpoint.get("getIntegrationToken", `/integrations/:orgId/:provider/token`) - .addSuccess(IntegrationTokenResponse) - .addError(UnauthorizedError) - .addError(BotNotInstalledError) - .addError(IntegrationNotConnectedError) - .addError(IntegrationNotAllowedError) - .addError(InternalServerError) - .setPath( - Schema.Struct({ - orgId: OrganizationId, - provider: IntegrationProvider, - }), - ) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.get("getIntegrationToken", `/integrations/:orgId/:provider/token`, { + params: { + orgId: OrganizationId, + provider: IntegrationProvider, + }, + success: IntegrationTokenResponse, + error: [ + UnauthorizedError, + BotNotInstalledError, + IntegrationNotConnectedError, + IntegrationNotAllowedError, + InternalServerError, + ], + }) + .annotateMerge( + OpenApi.annotations({ title: "Get Integration Token", description: "Get a valid OAuth access token for an integration provider. Bot must have the provider in its allowedIntegrations and be installed in the target org.", @@ -235,18 +238,15 @@ export class BotCommandsApiGroup extends HttpApiGroup.make("bot-commands") // Get enabled integrations (bot token auth) // Returns the intersection of bot's allowedIntegrations and org's active connections .add( - HttpApiEndpoint.get("getEnabledIntegrations", `/integrations/:orgId/enabled`) - .addSuccess(EnabledIntegrationsResponse) - .addError(UnauthorizedError) - .addError(BotNotInstalledError) - .addError(InternalServerError) - .setPath( - Schema.Struct({ - orgId: OrganizationId, - }), - ) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.get("getEnabledIntegrations", `/integrations/:orgId/enabled`, { + params: { + orgId: OrganizationId, + }, + success: EnabledIntegrationsResponse, + error: [UnauthorizedError, BotNotInstalledError, InternalServerError], + }) + .annotateMerge( + OpenApi.annotations({ title: "Get Enabled Integrations", description: "Get the list of integration providers enabled for the bot in the target org. Returns the intersection of the bot's allowedIntegrations and the org's active integration connections.", @@ -258,13 +258,13 @@ export class BotCommandsApiGroup extends HttpApiGroup.make("bot-commands") // Update bot settings (bot token auth) // Allows bots to update their own settings like mentionable flag .add( - HttpApiEndpoint.patch("updateBotSettings", `/settings`) - .addSuccess(UpdateBotSettingsResponse) - .addError(UnauthorizedError) - .addError(InternalServerError) - .setPayload(UpdateBotSettingsRequest) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.patch("updateBotSettings", `/settings`, { + payload: UpdateBotSettingsRequest, + success: UpdateBotSettingsResponse, + error: [UnauthorizedError, InternalServerError], + }) + .annotateMerge( + OpenApi.annotations({ title: "Update Bot Settings", description: "Update the bot's settings. Currently supports updating the mentionable flag which controls whether the bot can be @mentioned in messages.", diff --git a/packages/domain/src/http/chat-sync.ts b/packages/domain/src/http/chat-sync.ts index d40838b78..77551e548 100644 --- a/packages/domain/src/http/chat-sync.ts +++ b/packages/domain/src/http/chat-sync.ts @@ -17,27 +17,27 @@ import { RequiredScopes } from "../scopes/required-scopes" export class ChatSyncConnectionResponse extends Schema.Class( "ChatSyncConnectionResponse", )({ - data: ChatSyncConnection.Model.json, + data: ChatSyncConnection.Model.json as any, transactionId: TransactionId, }) {} export class ChatSyncConnectionListResponse extends Schema.Class( "ChatSyncConnectionListResponse", )({ - data: Schema.Array(ChatSyncConnection.Model.json), + data: Schema.Array(ChatSyncConnection.Model.json as any), }) {} export class ChatSyncChannelLinkResponse extends Schema.Class( "ChatSyncChannelLinkResponse", )({ - data: ChatSyncChannelLink.Model.json, + data: ChatSyncChannelLink.Model.json as any, transactionId: TransactionId, }) {} export class ChatSyncChannelLinkListResponse extends Schema.Class( "ChatSyncChannelLinkListResponse", )({ - data: Schema.Array(ChatSyncChannelLink.Model.json), + data: Schema.Array(ChatSyncChannelLink.Model.json as any), }) {} export class ChatSyncDeleteResponse extends Schema.Class("ChatSyncDeleteResponse")({ @@ -91,8 +91,8 @@ export class CreateChatSyncConnectionRequest extends Schema.Class( @@ -102,21 +102,19 @@ export class CreateChatSyncChannelLinkRequest extends Schema.Class 1 }), - perPage: Schema.optional(Schema.NumberFromString, { default: () => 30 }), - }), - ) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.get("getGitHubRepositories", `/:orgId/github/repositories`, { + params: { orgId: OrganizationId }, + query: { + page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => 1)), + perPage: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => 30)), + }, + success: GitHubRepositoriesResponse, + error: [IntegrationNotConnectedForPreviewError, UnauthorizedError, InternalServerError], + }) + .annotateMerge( + OpenApi.annotations({ title: "Get GitHub Repositories", description: "List repositories accessible to the GitHub App installation", summary: "List GitHub repositories", @@ -240,19 +211,13 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res .annotate(RequiredScopes, ["integration-connections:read"]), ) .add( - HttpApiEndpoint.get("getDiscordGuilds", `/:orgId/discord/guilds`) - .addSuccess(DiscordGuildsResponse) - .addError(IntegrationNotConnectedForPreviewError) - .addError(IntegrationResourceError) - .addError(UnauthorizedError) - .addError(InternalServerError) - .setPath( - Schema.Struct({ - orgId: OrganizationId, - }), - ) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.get("getDiscordGuilds", `/:orgId/discord/guilds`, { + params: { orgId: OrganizationId }, + success: DiscordGuildsResponse, + error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, UnauthorizedError, InternalServerError], + }) + .annotateMerge( + OpenApi.annotations({ title: "Get Discord Guilds", description: "List Discord guilds visible to the connected Discord account", summary: "List Discord guilds", @@ -261,20 +226,16 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res .annotate(RequiredScopes, ["integration-connections:read"]), ) .add( - HttpApiEndpoint.get("getDiscordGuildChannels", `/:orgId/discord/guilds/:guildId/channels`) - .addSuccess(DiscordGuildChannelsResponse) - .addError(IntegrationNotConnectedForPreviewError) - .addError(IntegrationResourceError) - .addError(UnauthorizedError) - .addError(InternalServerError) - .setPath( - Schema.Struct({ - orgId: OrganizationId, - guildId: Schema.String, - }), - ) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.get("getDiscordGuildChannels", `/:orgId/discord/guilds/:guildId/channels`, { + params: { + orgId: OrganizationId, + guildId: Schema.String, + }, + success: DiscordGuildChannelsResponse, + error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, UnauthorizedError, InternalServerError], + }) + .annotateMerge( + OpenApi.annotations({ title: "Get Discord Guild Channels", description: "List message-capable channels in a Discord guild using the bot token", summary: "List Discord guild channels", diff --git a/packages/domain/src/http/integrations.ts b/packages/domain/src/http/integrations.ts index accdfd06a..f0219644c 100644 --- a/packages/domain/src/http/integrations.ts +++ b/packages/domain/src/http/integrations.ts @@ -1,4 +1,4 @@ -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" import * as CurrentUser from "../current-user" import { InternalServerError, UnauthorizedError } from "../errors" @@ -22,8 +22,8 @@ export class ConnectionStatusResponse extends Schema.Class(" export class IntegrationGroup extends HttpApiGroup.make("integrations") // Initiate OAuth flow - returns authorization URL for SPA redirect .add( - HttpApiEndpoint.get("getOAuthUrl", `/:orgId/:provider/oauth`) - .addSuccess(OAuthUrlResponse) - .addError(UnsupportedProviderError) - .addError(UnauthorizedError) - .addError(InternalServerError) - .setPath( - Schema.Struct({ - orgId: OrganizationId, - provider: IntegrationProvider, - }), - ) - .setUrlParams( - Schema.Struct({ - level: Schema.optional(ConnectionLevel), - }), - ) + HttpApiEndpoint.get("getOAuthUrl", `/:orgId/:provider/oauth`, { + params: { + orgId: OrganizationId, + provider: IntegrationProvider, + }, + query: { + level: Schema.optional(ConnectionLevel), + }, + success: OAuthUrlResponse, + error: [UnsupportedProviderError, UnauthorizedError, InternalServerError], + }) .middleware(CurrentUser.Authorization) - .annotateContext( - OpenApi.annotate({ + .annotateMerge( + OpenApi.annotations({ title: "Get OAuth Authorization URL", description: "Returns the OAuth authorization URL for the provider. The frontend should redirect the user to this URL. Sets a session cookie to preserve context for the callback.", @@ -95,33 +90,26 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") ) // OAuth callback handler .add( - HttpApiEndpoint.get("oauthCallback", `/:provider/callback`) - .addSuccess(Schema.Void, { status: 302 }) - .addError(InvalidOAuthStateError) - .addError(UnsupportedProviderError) - .addError(InternalServerError) - .setPath( - Schema.Struct({ - provider: IntegrationProvider, - }), - ) - .setUrlParams( - Schema.Struct({ - // Standard OAuth uses `code` - code: Schema.optional(Schema.String), - // State is optional because GitHub doesn't send it for update callbacks - state: Schema.optional(Schema.String), - // Discord bot scope callback includes selected guild context - guild_id: Schema.optional(Schema.String), - permissions: Schema.optional(Schema.String), - // GitHub App uses `installation_id` instead of code - installation_id: Schema.optional(Schema.String), - // GitHub also sends setup_action (e.g., "install", "update") - setup_action: Schema.optional(Schema.String), - }), - ) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.get("oauthCallback", `/:provider/callback`, { + params: { provider: IntegrationProvider }, + query: { + // Standard OAuth uses `code` + code: Schema.optional(Schema.String), + // State is optional because GitHub doesn't send it for update callbacks + state: Schema.optional(Schema.String), + // Discord bot scope callback includes selected guild context + guild_id: Schema.optional(Schema.String), + permissions: Schema.optional(Schema.String), + // GitHub App uses `installation_id` instead of code + installation_id: Schema.optional(Schema.String), + // GitHub also sends setup_action (e.g., "install", "update") + setup_action: Schema.optional(Schema.String), + }, + success: Schema.Void.pipe(HttpApiSchema.status(302)), + error: [InvalidOAuthStateError, UnsupportedProviderError, InternalServerError], + }) + .annotateMerge( + OpenApi.annotations({ title: "OAuth Callback", description: "Handle OAuth callback from integration provider", summary: "Process OAuth callback", @@ -131,25 +119,20 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") ) // Get connection status .add( - HttpApiEndpoint.get("getConnectionStatus", `/:orgId/:provider/status`) - .addSuccess(ConnectionStatusResponse) - .addError(UnsupportedProviderError) - .addError(UnauthorizedError) - .addError(InternalServerError) - .setPath( - Schema.Struct({ - orgId: OrganizationId, - provider: IntegrationProvider, - }), - ) - .setUrlParams( - Schema.Struct({ - level: Schema.optional(ConnectionLevel), - }), - ) + HttpApiEndpoint.get("getConnectionStatus", `/:orgId/:provider/status`, { + params: { + orgId: OrganizationId, + provider: IntegrationProvider, + }, + query: { + level: Schema.optional(ConnectionLevel), + }, + success: ConnectionStatusResponse, + error: [UnsupportedProviderError, UnauthorizedError, InternalServerError], + }) .middleware(CurrentUser.Authorization) - .annotateContext( - OpenApi.annotate({ + .annotateMerge( + OpenApi.annotations({ title: "Get Connection Status", description: "Check the connection status for a provider", summary: "Get integration status", @@ -159,22 +142,18 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") ) // Connect via API key (non-OAuth providers like Craft) .add( - HttpApiEndpoint.post("connectApiKey", `/:orgId/:provider/api-key`) - .addSuccess(ConnectApiKeyResponse) - .addError(InvalidApiKeyError) - .addError(UnsupportedProviderError) - .addError(UnauthorizedError) - .addError(InternalServerError) - .setPath( - Schema.Struct({ - orgId: OrganizationId, - provider: IntegrationProvider, - }), - ) - .setPayload(ConnectApiKeyRequest) + HttpApiEndpoint.post("connectApiKey", `/:orgId/:provider/api-key`, { + params: { + orgId: OrganizationId, + provider: IntegrationProvider, + }, + payload: ConnectApiKeyRequest, + success: ConnectApiKeyResponse, + error: [InvalidApiKeyError, UnsupportedProviderError, UnauthorizedError, InternalServerError], + }) .middleware(CurrentUser.Authorization) - .annotateContext( - OpenApi.annotate({ + .annotateMerge( + OpenApi.annotations({ title: "Connect via API Key", description: "Connect an integration using an API key/token instead of OAuth. Validates the credentials against the provider and stores the connection.", @@ -185,25 +164,19 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") ) // Disconnect integration .add( - HttpApiEndpoint.del("disconnect", `/:orgId/:provider`) - .addSuccess(Schema.Void) - .addError(IntegrationNotConnectedError) - .addError(UnsupportedProviderError) - .addError(UnauthorizedError) - .addError(InternalServerError) - .setPath( - Schema.Struct({ - orgId: OrganizationId, - provider: IntegrationProvider, - }), - ) - .setUrlParams( - Schema.Struct({ - level: Schema.optional(ConnectionLevel), - }), - ) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.delete("disconnect", `/:orgId/:provider`, { + params: { + orgId: OrganizationId, + provider: IntegrationProvider, + }, + query: { + level: Schema.optional(ConnectionLevel), + }, + success: Schema.Void, + error: [IntegrationNotConnectedError, UnsupportedProviderError, UnauthorizedError, InternalServerError], + }) + .annotateMerge( + OpenApi.annotations({ title: "Disconnect Integration", description: "Disconnect an integration and revoke tokens", summary: "Disconnect provider", diff --git a/packages/domain/src/http/internal.ts b/packages/domain/src/http/internal.ts index fec8eb9b0..47264414f 100644 --- a/packages/domain/src/http/internal.ts +++ b/packages/domain/src/http/internal.ts @@ -26,14 +26,13 @@ export class ValidateBotTokenResponse extends Schema.Class("Klipy export class KlipyGroup extends HttpApiGroup.make("klipy") .add( - HttpApiEndpoint.get("trending", "/trending") - .setUrlParams( - Schema.Struct({ - page: Schema.optional(Schema.NumberFromString, { default: () => 1 }), - per_page: Schema.optional(Schema.NumberFromString, { default: () => 25 }), - }), - ) - .addSuccess(KlipySearchResponse) - .addError(KlipyApiError) + HttpApiEndpoint.get("trending", "/trending", { + query: { + page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => 1)), + per_page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => 25)), + }, + success: KlipySearchResponse, + error: KlipyApiError, + }) .annotate(RequiredScopes, ["messages:read"]), ) .add( - HttpApiEndpoint.get("search", "/search") - .setUrlParams( - Schema.Struct({ - q: Schema.String, - page: Schema.optional(Schema.NumberFromString, { default: () => 1 }), - per_page: Schema.optional(Schema.NumberFromString, { default: () => 25 }), - }), - ) - .addSuccess(KlipySearchResponse) - .addError(KlipyApiError) + HttpApiEndpoint.get("search", "/search", { + query: { + q: Schema.String, + page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => 1)), + per_page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => 25)), + }, + success: KlipySearchResponse, + error: KlipyApiError, + }) .annotate(RequiredScopes, ["messages:read"]), ) .add( - HttpApiEndpoint.get("categories", "/categories") - .addSuccess(KlipyCategoriesResponse) - .addError(KlipyApiError) + HttpApiEndpoint.get("categories", "/categories", { + success: KlipyCategoriesResponse, + error: KlipyApiError, + }) .annotate(RequiredScopes, ["messages:read"]), ) .prefix("/klipy") diff --git a/packages/domain/src/http/mock-data.ts b/packages/domain/src/http/mock-data.ts index a0acb6952..b975ad07e 100644 --- a/packages/domain/src/http/mock-data.ts +++ b/packages/domain/src/http/mock-data.ts @@ -28,13 +28,13 @@ export class GenerateMockDataResponse extends Schema.Class("Mark export class PresencePublicGroup extends HttpApiGroup.make("presencePublic") .add( - HttpApiEndpoint.post("markOffline")`/offline` - .setPayload(MarkOfflinePayload) - .addSuccess(MarkOfflineResponse) - .addError(InternalServerError) - .annotateContext( - OpenApi.annotate({ + HttpApiEndpoint.post("markOffline", "/offline", { + payload: MarkOfflinePayload, + success: MarkOfflineResponse, + error: InternalServerError, + }) + .annotateMerge( + OpenApi.annotations({ title: "Mark User Offline", description: "Mark a user as offline when they close their tab (no auth required)", summary: "Mark offline", diff --git a/packages/domain/src/http/root.ts b/packages/domain/src/http/root.ts index 8fd6b8e5e..0a11a7b9f 100644 --- a/packages/domain/src/http/root.ts +++ b/packages/domain/src/http/root.ts @@ -3,5 +3,5 @@ import { Schema } from "effect" import { RequiredScopes } from "../scopes/required-scopes" export class RootGroup extends HttpApiGroup.make("root").add( - HttpApiEndpoint.get("root")`/`.addSuccess(Schema.String).annotate(RequiredScopes, []), + HttpApiEndpoint.get("root", "/", { success: Schema.String }).annotate(RequiredScopes, []), ) {} diff --git a/packages/domain/src/http/uploads.ts b/packages/domain/src/http/uploads.ts index 65eccc471..de7f25285 100644 --- a/packages/domain/src/http/uploads.ts +++ b/packages/domain/src/http/uploads.ts @@ -35,19 +35,27 @@ const BaseUploadFields = { fileSize: Schema.Number, } +const allowedAvatarTypeFilter = Schema.makeFilter( + (s) => ALLOWED_AVATAR_TYPES.includes(s as (typeof ALLOWED_AVATAR_TYPES)[number]) + ? undefined + : "Content type must be image/jpeg, image/png, or image/webp", +) + +const allowedEmojiTypeFilter = Schema.makeFilter( + (s) => ALLOWED_EMOJI_TYPES.includes(s as (typeof ALLOWED_EMOJI_TYPES)[number]) + ? undefined + : "Content type must be image/png, image/gif, or image/webp", +) + /** * User avatar upload request */ export class UserAvatarUploadRequest extends Schema.Class("UserAvatarUploadRequest")( { type: Schema.Literal("user-avatar"), - contentType: Schema.String.pipe( - Schema.filter((s) => ALLOWED_AVATAR_TYPES.includes(s as (typeof ALLOWED_AVATAR_TYPES)[number]), { - message: () => "Content type must be image/jpeg, image/png, or image/webp", - }), - ), - fileSize: Schema.Number.pipe( - Schema.isBetween(1, MAX_AVATAR_SIZE, { + contentType: Schema.String.check(allowedAvatarTypeFilter), + fileSize: Schema.Number.check( + Schema.isBetween({ minimum: 1, maximum: MAX_AVATAR_SIZE }, { message: () => "File size must be between 1 byte and 5MB", }), ), @@ -60,13 +68,9 @@ export class UserAvatarUploadRequest extends Schema.Class("BotAvatarUploadRequest")({ type: Schema.Literal("bot-avatar"), botId: BotId, - contentType: Schema.String.pipe( - Schema.filter((s) => ALLOWED_AVATAR_TYPES.includes(s as (typeof ALLOWED_AVATAR_TYPES)[number]), { - message: () => "Content type must be image/jpeg, image/png, or image/webp", - }), - ), - fileSize: Schema.Number.pipe( - Schema.isBetween(1, MAX_AVATAR_SIZE, { + contentType: Schema.String.check(allowedAvatarTypeFilter), + fileSize: Schema.Number.check( + Schema.isBetween({ minimum: 1, maximum: MAX_AVATAR_SIZE }, { message: () => "File size must be between 1 byte and 5MB", }), ), @@ -80,13 +84,9 @@ export class OrganizationAvatarUploadRequest extends Schema.Class ALLOWED_AVATAR_TYPES.includes(s as (typeof ALLOWED_AVATAR_TYPES)[number]), { - message: () => "Content type must be image/jpeg, image/png, or image/webp", - }), - ), - fileSize: Schema.Number.pipe( - Schema.isBetween(1, MAX_AVATAR_SIZE, { + contentType: Schema.String.check(allowedAvatarTypeFilter), + fileSize: Schema.Number.check( + Schema.isBetween({ minimum: 1, maximum: MAX_AVATAR_SIZE }, { message: () => "File size must be between 1 byte and 5MB", }), ), @@ -100,8 +100,8 @@ export class AttachmentUploadRequest extends Schema.Class "File size must be between 1 byte and 10MB", }), ), @@ -118,13 +118,9 @@ export class CustomEmojiUploadRequest extends Schema.Class ALLOWED_EMOJI_TYPES.includes(s as (typeof ALLOWED_EMOJI_TYPES)[number]), { - message: () => "Content type must be image/png, image/gif, or image/webp", - }), - ), - fileSize: Schema.Number.pipe( - Schema.isBetween(1, MAX_EMOJI_SIZE, { + contentType: Schema.String.check(allowedEmojiTypeFilter), + fileSize: Schema.Number.check( + Schema.isBetween({ minimum: 1, maximum: MAX_EMOJI_SIZE }, { message: () => "File size must be between 1 byte and 256KB", }), ), @@ -199,15 +195,18 @@ export class OrganizationNotFoundForUploadError extends Schema.TaggedErrorClass< */ export class UploadsGroup extends HttpApiGroup.make("uploads") .add( - HttpApiEndpoint.post("presign", "/presign") - .setPayload(PresignUploadRequest) - .addSuccess(PresignUploadResponse) - .addError(UploadError) - .addError(BotNotFoundForUploadError) - .addError(OrganizationNotFoundForUploadError) - .addError(UnauthorizedError) - .addError(InternalServerError) - .addError(RateLimitExceededError) + HttpApiEndpoint.post("presign", "/presign", { + payload: PresignUploadRequest, + success: PresignUploadResponse, + error: [ + UploadError, + BotNotFoundForUploadError, + OrganizationNotFoundForUploadError, + UnauthorizedError, + InternalServerError, + RateLimitExceededError, + ], + }) .annotate(RequiredScopes, ["attachments:write"]), ) .prefix("/uploads") diff --git a/packages/domain/src/http/webhooks.ts b/packages/domain/src/http/webhooks.ts index 3b1d7da03..bc72fd5fc 100644 --- a/packages/domain/src/http/webhooks.ts +++ b/packages/domain/src/http/webhooks.ts @@ -44,13 +44,13 @@ export class InvalidGitHubWebhookSignature extends Schema.TaggedErrorClass - Layer.unwrapEffect( + Layer.unwrap( Effect.gen(function* () { const environment = yield* Config.string("OTEL_ENVIRONMENT").pipe(Config.withDefault("local")) const commitSha = yield* Config.string("RAILWAY_GIT_COMMIT_SHA").pipe( @@ -46,12 +46,12 @@ export const createTracingLayer = (otelServiceName: string) => ) } - return DevTools.layerWebSocket().pipe(Layer.provide(BunSocket.layerWebSocketConstructor)) + return DevTools.layerSocket.pipe(Layer.provide(BunSocket.layerWebSocketConstructor)) } const otelBaseUrl = yield* Config.string("OTEL_BASE_URL") - return Otlp.layerJson({ + return Otlp.layer({ baseUrl: otelBaseUrl, resource: { serviceName: otelServiceName, diff --git a/packages/effect-bun/src/persistence/redis-backing.ts b/packages/effect-bun/src/persistence/redis-backing.ts index 68bead25c..081804b96 100644 --- a/packages/effect-bun/src/persistence/redis-backing.ts +++ b/packages/effect-bun/src/persistence/redis-backing.ts @@ -11,7 +11,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { const redis = yield* Redis return Persistence.BackingPersistence.of({ - [Persistence.BackingPersistenceTypeId]: Persistence.BackingPersistenceTypeId, + [Persistence.BackingPersistence]: Persistence.BackingPersistence, make: (prefix) => Effect.sync(() => { const prefixed = (key: string) => `${prefix}:${key}` @@ -20,7 +20,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { if (str === null) return Effect.succeedNone return Effect.try({ try: () => Option.some(JSON.parse(str)), - catch: (error) => Persistence.PersistenceBackingError.make(method, error), + catch: (error) => Persistence.PersistenceError.make(method, error), }) } @@ -31,7 +31,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .get(prefixed(key)) .pipe( Effect.mapError((error) => - Persistence.PersistenceBackingError.make("get", error), + Persistence.PersistenceError.make("get", error), ), ), parse("get"), @@ -43,7 +43,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .send<(string | null)[]>("MGET", keys.map(prefixed)) .pipe( Effect.mapError((error) => - Persistence.PersistenceBackingError.make("getMany", error), + Persistence.PersistenceError.make("getMany", error), ), ), Effect.forEach(parse("getMany")), @@ -53,7 +53,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { Effect.gen(function* () { const serialized = yield* Effect.try({ try: () => JSON.stringify(value), - catch: (error) => Persistence.PersistenceBackingError.make("set", error), + catch: (error) => Persistence.PersistenceError.make("set", error), }) const pkey = prefixed(key) @@ -68,7 +68,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { ]) .pipe( Effect.mapError((error) => - Persistence.PersistenceBackingError.make("set", error), + Persistence.PersistenceError.make("set", error), ), ) } else { @@ -76,7 +76,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .set(pkey, serialized) .pipe( Effect.mapError((error) => - Persistence.PersistenceBackingError.make("set", error), + Persistence.PersistenceError.make("set", error), ), ) } @@ -97,7 +97,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { ]) .pipe( Effect.mapError((error) => - Persistence.PersistenceBackingError.make("setMany", error), + Persistence.PersistenceError.make("setMany", error), ), ) } else { @@ -105,7 +105,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .set(pkey, serialized) .pipe( Effect.mapError((error) => - Persistence.PersistenceBackingError.make("setMany", error), + Persistence.PersistenceError.make("setMany", error), ), ) } @@ -117,7 +117,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .del(prefixed(key)) .pipe( Effect.mapError((error) => - Persistence.PersistenceBackingError.make("remove", error), + Persistence.PersistenceError.make("remove", error), ), ), @@ -126,7 +126,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .send("KEYS", [`${prefix}:*`]) .pipe( Effect.mapError((error) => - Persistence.PersistenceBackingError.make("clear", error), + Persistence.PersistenceError.make("clear", error), ), ) if (keys.length > 0) { @@ -134,7 +134,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .send("DEL", keys) .pipe( Effect.mapError((error) => - Persistence.PersistenceBackingError.make("clear", error), + Persistence.PersistenceError.make("clear", error), ), ) } From 890295b3a8f2af99c93a0108f3848d1f3c8b0bbe Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 00:32:11 +0100 Subject: [PATCH 06/34] fix --- apps/backend/src/rpc/middleware/auth.test.ts | 2 +- apps/electric-proxy/src/cache/redis-persistence.ts | 2 +- libs/bot-sdk/src/rpc/client.ts | 2 +- packages/db/src/services/database.ts | 2 +- packages/domain/src/http/mock-data.ts | 2 +- packages/domain/src/http/uploads.ts | 10 +++++----- packages/effect-bun/src/Redis.ts | 8 ++++---- packages/effect-bun/src/persistence/redis-backing.ts | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/backend/src/rpc/middleware/auth.test.ts b/apps/backend/src/rpc/middleware/auth.test.ts index fc3f044a9..6edc5e5f8 100644 --- a/apps/backend/src/rpc/middleware/auth.test.ts +++ b/apps/backend/src/rpc/middleware/auth.test.ts @@ -80,7 +80,7 @@ const makeAuthMiddlewareLayer = (options?: { const sessionManagerLayer = options?.sessionManagerLayer ?? createMockSessionManagerLive() const presenceRepoLayer = options?.presenceRepoLayer ?? createMockPresenceRepoLive() - return Layer.scoped( + return Layer.effect( AuthMiddleware, Effect.gen(function* () { const sessionManager = yield* SessionManager diff --git a/apps/electric-proxy/src/cache/redis-persistence.ts b/apps/electric-proxy/src/cache/redis-persistence.ts index 5da8b9278..8413ea96a 100644 --- a/apps/electric-proxy/src/cache/redis-persistence.ts +++ b/apps/electric-proxy/src/cache/redis-persistence.ts @@ -18,4 +18,4 @@ export const RedisPersistenceLive = Layer.unwrapEffect( /** * In-memory persistence layer for testing or fallback. */ -export const MemoryPersistenceLive = Persistence.layerResultMemory +export const MemoryPersistenceLive = Persistence.layerMemory diff --git a/libs/bot-sdk/src/rpc/client.ts b/libs/bot-sdk/src/rpc/client.ts index cfc938169..cfde3050f 100644 --- a/libs/bot-sdk/src/rpc/client.ts +++ b/libs/bot-sdk/src/rpc/client.ts @@ -51,7 +51,7 @@ export class BotRpcClient extends ServiceMap.Service> export class Database extends Effect.Tag("Database")() {} -export const layer = (config: Config) => Layer.scoped(Database, makeService(config)) +export const layer = (config: Config) => Layer.effect(Database, makeService(config)) diff --git a/packages/domain/src/http/mock-data.ts b/packages/domain/src/http/mock-data.ts index b975ad07e..474e9687b 100644 --- a/packages/domain/src/http/mock-data.ts +++ b/packages/domain/src/http/mock-data.ts @@ -7,7 +7,7 @@ import { RequiredScopes } from "../scopes/required-scopes" export class GenerateMockDataRequest extends Schema.Class("GenerateMockDataRequest")( { - organizationId: Schema.UUID, + organizationId: Schema.String.check(Schema.isUUID()), }, ) {} diff --git a/packages/domain/src/http/uploads.ts b/packages/domain/src/http/uploads.ts index de7f25285..f6eda3cc4 100644 --- a/packages/domain/src/http/uploads.ts +++ b/packages/domain/src/http/uploads.ts @@ -56,7 +56,7 @@ export class UserAvatarUploadRequest extends Schema.Class "File size must be between 1 byte and 5MB", + message: "File size must be between 1 byte and 5MB", }), ), }, @@ -71,7 +71,7 @@ export class BotAvatarUploadRequest extends Schema.Class contentType: Schema.String.check(allowedAvatarTypeFilter), fileSize: Schema.Number.check( Schema.isBetween({ minimum: 1, maximum: MAX_AVATAR_SIZE }, { - message: () => "File size must be between 1 byte and 5MB", + message: "File size must be between 1 byte and 5MB", }), ), }) {} @@ -87,7 +87,7 @@ export class OrganizationAvatarUploadRequest extends Schema.Class "File size must be between 1 byte and 5MB", + message: "File size must be between 1 byte and 5MB", }), ), }) {} @@ -102,7 +102,7 @@ export class AttachmentUploadRequest extends Schema.Class "File size must be between 1 byte and 10MB", + message: "File size must be between 1 byte and 10MB", }), ), organizationId: OrganizationId, @@ -121,7 +121,7 @@ export class CustomEmojiUploadRequest extends Schema.Class "File size must be between 1 byte and 256KB", + message: "File size must be between 1 byte and 256KB", }), ), }) {} diff --git a/packages/effect-bun/src/Redis.ts b/packages/effect-bun/src/Redis.ts index 39b39bc91..5645d93f0 100644 --- a/packages/effect-bun/src/Redis.ts +++ b/packages/effect-bun/src/Redis.ts @@ -227,7 +227,7 @@ export class Redis extends ServiceMap.Service - Layer.scoped( + Layer.effect( Redis, Effect.gen(function* () { const client = new RedisClient(url) @@ -236,7 +236,7 @@ export class Redis extends ServiceMap.Service client.connect(), catch: mapRedisError, }).pipe( - Effect.timeoutFail({ + Effect.timeoutOrElse({ duration: Duration.seconds(10), onTimeout: () => new RedisError({ @@ -260,7 +260,7 @@ export class Redis extends ServiceMap.Service client.connect(), catch: mapRedisError, }).pipe( - Effect.timeoutFail({ + Effect.timeoutOrElse({ duration: Duration.seconds(10), onTimeout: () => new RedisError({ diff --git a/packages/effect-bun/src/persistence/redis-backing.ts b/packages/effect-bun/src/persistence/redis-backing.ts index 081804b96..8d7563546 100644 --- a/packages/effect-bun/src/persistence/redis-backing.ts +++ b/packages/effect-bun/src/persistence/redis-backing.ts @@ -167,4 +167,4 @@ export const RedisResultPersistenceLive = Persistence.layerResult.pipe( * In-memory persistence layer for testing or fallback. * Provides: Persistence.ResultPersistence */ -export const MemoryResultPersistenceLive = Persistence.layerResultMemory +export const MemoryResultPersistenceLive = Persistence.layerMemory From 479dd02aa055dcb2d943ebce2f82d6999dddf93b Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 00:38:12 +0100 Subject: [PATCH 07/34] fix schema stufff --- apps/bot-gateway/src/index.ts | 6 +- bots/hazel-bot/src/agent-loop.ts | 6 +- bots/hazel-bot/src/handler.ts | 6 +- .../domain/src/http/integration-resources.ts | 4 +- packages/domain/src/http/klipy.ts | 8 +-- packages/domain/src/models/bot-model.ts | 5 +- .../models/chat-sync-channel-link-model.ts | 19 +++-- .../src/models/chat-sync-connection-model.ts | 6 +- .../src/models/connect-conversation-model.ts | 2 +- .../models/integration-connection-model.ts | 4 +- .../src/models/integration-request-model.ts | 2 +- .../domain/src/models/message-embed-schema.ts | 36 +++++----- .../models/message-integration-link-model.ts | 2 +- packages/domain/src/models/message-model.ts | 5 +- .../domain/src/models/notification-model.ts | 4 +- .../domain/src/models/organization-model.ts | 5 +- packages/domain/src/models/theme-model.ts | 8 +-- packages/domain/src/models/user-model.ts | 9 ++- packages/domain/src/rpc/channel-members.ts | 2 +- packages/domain/src/rpc/channel-sections.ts | 2 +- packages/domain/src/rpc/channels.ts | 14 ++-- packages/domain/src/rpc/chat-sync.ts | 6 +- .../domain/src/rpc/integration-requests.ts | 2 +- packages/domain/src/rpc/messages.ts | 2 +- packages/domain/src/rpc/organizations.ts | 2 +- packages/domain/src/rpc/typing-indicators.ts | 4 +- packages/domain/src/rpc/users.ts | 2 +- packages/effect-bun/src/Redis.ts | 8 +-- packages/effect-bun/src/Telemetry.ts | 7 +- .../src/persistence/redis-backing.ts | 70 ++++++++++--------- 30 files changed, 128 insertions(+), 130 deletions(-) diff --git a/apps/bot-gateway/src/index.ts b/apps/bot-gateway/src/index.ts index 466e669b6..92629f392 100644 --- a/apps/bot-gateway/src/index.ts +++ b/apps/bot-gateway/src/index.ts @@ -475,11 +475,11 @@ class BotGatewayHub extends ServiceMap.Service()("BotGatewayHub", }) const ackResult = yield* Deferred.await(ackDeferred).pipe( - Effect.timeoutFail({ + Effect.timeoutOrElse({ onTimeout: () => - new GatewayProtocolError({ + Effect.fail(new GatewayProtocolError({ message: `Timed out waiting for ACK from session ${id}`, - }), + })), duration: config.batchAckTimeoutMs, }), ) diff --git a/bots/hazel-bot/src/agent-loop.ts b/bots/hazel-bot/src/agent-loop.ts index 38beed039..48325a79f 100644 --- a/bots/hazel-bot/src/agent-loop.ts +++ b/bots/hazel-bot/src/agent-loop.ts @@ -64,11 +64,11 @@ export const streamAgentLoop = (options: { return mailbox.offer(part as Response.AnyPart) }), // Iteration timeout: wall-clock limit per LLM call - Effect.timeoutFail({ + Effect.timeoutOrElse({ onTimeout: () => - new IterationTimeoutError({ + Effect.fail(new IterationTimeoutError({ message: "Single LLM call exceeded 2 minute time limit", - }), + })), duration: ITERATION_TIMEOUT, }), ) diff --git a/bots/hazel-bot/src/handler.ts b/bots/hazel-bot/src/handler.ts index 0641bb850..70baeca51 100644 --- a/bots/hazel-bot/src/handler.ts +++ b/bots/hazel-bot/src/handler.ts @@ -113,11 +113,11 @@ export const handleAIRequest = (params: { yield* session.complete() yield* Effect.log(`Agent response complete: ${session.messageId}`) }).pipe( - Effect.timeoutFail({ + Effect.timeoutOrElse({ onTimeout: () => - new SessionTimeoutError({ + Effect.fail(new SessionTimeoutError({ message: "Overall AI session exceeded 3 minute time limit", - }), + })), duration: Duration.minutes(3), }), ), diff --git a/packages/domain/src/http/integration-resources.ts b/packages/domain/src/http/integration-resources.ts index f8f8e384c..09fae9f23 100644 --- a/packages/domain/src/http/integration-resources.ts +++ b/packages/domain/src/http/integration-resources.ts @@ -195,8 +195,8 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res HttpApiEndpoint.get("getGitHubRepositories", `/:orgId/github/repositories`, { params: { orgId: OrganizationId }, query: { - page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => 1)), - perPage: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => 30)), + page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => "1")), + perPage: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => "30")), }, success: GitHubRepositoriesResponse, error: [IntegrationNotConnectedForPreviewError, UnauthorizedError, InternalServerError], diff --git a/packages/domain/src/http/klipy.ts b/packages/domain/src/http/klipy.ts index 5e12bbeae..5c81812ac 100644 --- a/packages/domain/src/http/klipy.ts +++ b/packages/domain/src/http/klipy.ts @@ -71,8 +71,8 @@ export class KlipyGroup extends HttpApiGroup.make("klipy") .add( HttpApiEndpoint.get("trending", "/trending", { query: { - page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => 1)), - per_page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => 25)), + page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => "1")), + per_page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => "25")), }, success: KlipySearchResponse, error: KlipyApiError, @@ -83,8 +83,8 @@ export class KlipyGroup extends HttpApiGroup.make("klipy") HttpApiEndpoint.get("search", "/search", { query: { q: Schema.String, - page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => 1)), - per_page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => 25)), + page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => "1")), + per_page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => "25")), }, success: KlipySearchResponse, error: KlipyApiError, diff --git a/packages/domain/src/models/bot-model.ts b/packages/domain/src/models/bot-model.ts index 2261bd2aa..55ceece16 100644 --- a/packages/domain/src/models/bot-model.ts +++ b/packages/domain/src/models/bot-model.ts @@ -14,10 +14,7 @@ export class Model extends M.Class("Bot")({ apiTokenHash: Schema.String, scopes: Schema.NullOr(Schema.Array(Schema.String)), metadata: Schema.NullOr( - Schema.Record({ - key: Schema.String, - value: Schema.Unknown, - }), + Schema.Record(Schema.String, Schema.Unknown), ), isPublic: Schema.Boolean, installCount: Schema.Number, diff --git a/packages/domain/src/models/chat-sync-channel-link-model.ts b/packages/domain/src/models/chat-sync-channel-link-model.ts index 3ad3bc284..87b52d728 100644 --- a/packages/domain/src/models/chat-sync-channel-link-model.ts +++ b/packages/domain/src/models/chat-sync-channel-link-model.ts @@ -11,9 +11,9 @@ export type ChatSyncOutboundIdentityStrategy = Schema.Schema.Type @@ -30,15 +30,12 @@ export const ProviderOutboundConfig = Schema.Union([ DiscordWebhookOutboundIdentityConfig, SlackWebhookOutboundIdentityConfig, Schema.Struct({ - kind: Schema.NonEmptyTrimmedString, + kind: Schema.NonEmptyString, }), ]) export type ProviderOutboundConfig = Schema.Schema.Type -export const OutboundIdentityProviders = Schema.Record({ - key: Schema.String, - value: ProviderOutboundConfig, -}) +export const OutboundIdentityProviders = Schema.Record(Schema.String, ProviderOutboundConfig) export const OutboundIdentitySettings = Schema.Struct({ enabled: Schema.Boolean, @@ -55,7 +52,7 @@ export class Model extends M.Class("ChatSyncChannelLink")({ externalChannelName: Schema.NullOr(Schema.String), direction: ChatSyncDirection, isActive: Schema.Boolean, - settings: Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + settings: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), lastSyncedAt: Schema.NullOr(JsonDate), createdAt: M.Generated(JsonDate), updatedAt: M.Generated(Schema.NullOr(JsonDate)), diff --git a/packages/domain/src/models/chat-sync-connection-model.ts b/packages/domain/src/models/chat-sync-connection-model.ts index 7b48e887a..dcd39c484 100644 --- a/packages/domain/src/models/chat-sync-connection-model.ts +++ b/packages/domain/src/models/chat-sync-connection-model.ts @@ -3,7 +3,7 @@ import { Schema } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const ChatSyncProvider = Schema.NonEmptyTrimmedString +export const ChatSyncProvider = Schema.NonEmptyString export type ChatSyncProvider = Schema.Schema.Type export const ChatSyncConnectionStatus = Schema.Literals(["active", "paused", "error", "disabled"]) @@ -17,8 +17,8 @@ export class Model extends M.Class("ChatSyncConnection")({ externalWorkspaceId: Schema.String, externalWorkspaceName: Schema.NullOr(Schema.String), status: ChatSyncConnectionStatus, - settings: Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.Unknown })), - metadata: Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + settings: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), errorMessage: Schema.NullOr(Schema.String), lastSyncedAt: Schema.NullOr(JsonDate), createdBy: UserId, diff --git a/packages/domain/src/models/connect-conversation-model.ts b/packages/domain/src/models/connect-conversation-model.ts index 0f0647f4f..034f6a58f 100644 --- a/packages/domain/src/models/connect-conversation-model.ts +++ b/packages/domain/src/models/connect-conversation-model.ts @@ -11,7 +11,7 @@ export class Model extends M.Class("ConnectConversation")({ hostOrganizationId: OrganizationId, hostChannelId: ChannelId, status: ConnectConversationStatus, - settings: Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + settings: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), createdBy: UserId, createdAt: M.Generated(JsonDate), updatedAt: M.Generated(Schema.NullOr(JsonDate)), diff --git a/packages/domain/src/models/integration-connection-model.ts b/packages/domain/src/models/integration-connection-model.ts index 58c66f4c5..181141a01 100644 --- a/packages/domain/src/models/integration-connection-model.ts +++ b/packages/domain/src/models/integration-connection-model.ts @@ -22,8 +22,8 @@ export class Model extends M.Class("IntegrationConnection")({ externalAccountId: Schema.NullOr(Schema.String), externalAccountName: Schema.NullOr(Schema.String), connectedBy: UserId, - settings: Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.Unknown })), - metadata: Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + settings: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), errorMessage: Schema.NullOr(Schema.String), lastUsedAt: Schema.NullOr(JsonDate), createdAt: M.Generated(JsonDate), diff --git a/packages/domain/src/models/integration-request-model.ts b/packages/domain/src/models/integration-request-model.ts index 72c8646af..79194c4c8 100644 --- a/packages/domain/src/models/integration-request-model.ts +++ b/packages/domain/src/models/integration-request-model.ts @@ -10,7 +10,7 @@ export class Model extends M.Class("IntegrationRequest")({ id: M.Generated(IntegrationRequestId), organizationId: OrganizationId, requestedBy: UserId, - integrationName: Schema.NonEmptyTrimmedString, + integrationName: Schema.NonEmptyString, integrationUrl: Schema.NullOr(Schema.String), description: Schema.NullOr(Schema.String), status: IntegrationRequestStatus, diff --git a/packages/domain/src/models/message-embed-schema.ts b/packages/domain/src/models/message-embed-schema.ts index a0b91fbcb..36e9f0a2a 100644 --- a/packages/domain/src/models/message-embed-schema.ts +++ b/packages/domain/src/models/message-embed-schema.ts @@ -2,16 +2,16 @@ import { Schema } from "effect" // Embed author section export const MessageEmbedAuthor = Schema.Struct({ - name: Schema.String.pipe(Schema.isMaxLength(256)), - url: Schema.optional(Schema.String.pipe(Schema.isMaxLength(2048))), - iconUrl: Schema.optional(Schema.String.pipe(Schema.isMaxLength(2048))), + name: Schema.String.check(Schema.isMaxLength(256)), + url: Schema.optional(Schema.String.check(Schema.isMaxLength(2048))), + iconUrl: Schema.optional(Schema.String.check(Schema.isMaxLength(2048))), }) export type MessageEmbedAuthor = Schema.Schema.Type // Embed footer section export const MessageEmbedFooter = Schema.Struct({ - text: Schema.String.pipe(Schema.isMaxLength(2048)), - iconUrl: Schema.optional(Schema.String.pipe(Schema.isMaxLength(2048))), + text: Schema.String.check(Schema.isMaxLength(2048)), + iconUrl: Schema.optional(Schema.String.check(Schema.isMaxLength(2048))), }) export type MessageEmbedFooter = Schema.Schema.Type @@ -39,8 +39,8 @@ export type MessageEmbedFieldOptions = Schema.Schema.Type // Embed badge (for status indicators) export const MessageEmbedBadge = Schema.Struct({ - text: Schema.String.pipe(Schema.isMinLength(1), Schema.isMaxLength(64)), - color: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.isBetween(0, 16777215))), // 0x000000 to 0xFFFFFF + text: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(64)), + color: Schema.optional(Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 16777215 }))), // 0x000000 to 0xFFFFFF }) export type MessageEmbedBadge = Schema.Schema.Type @@ -61,7 +61,7 @@ export const CachedAgentStep = Schema.Struct({ status: Schema.Literals(["pending", "active", "completed", "failed"]), content: Schema.optional(Schema.String), toolName: Schema.optional(Schema.String), - toolInput: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + toolInput: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), toolOutput: Schema.optional(Schema.Unknown), toolError: Schema.optional(Schema.String), startedAt: Schema.optional(Schema.Number), @@ -72,7 +72,7 @@ export type CachedAgentStep = Schema.Schema.Type // Live state cached snapshot for non-realtime clients export const MessageEmbedLiveStateCached = Schema.Struct({ status: Schema.Literals(["idle", "active", "completed", "failed"]), - data: Schema.Record({ key: Schema.String, value: Schema.Unknown }), + data: Schema.Record(Schema.String, Schema.Unknown), text: Schema.optional(Schema.String), progress: Schema.optional(Schema.Number), error: Schema.optional(Schema.String), @@ -104,15 +104,15 @@ export type MessageEmbedLiveState = Schema.Schema.Type("MessageIntegrationLink")({ externalUrl: Schema.String, externalTitle: Schema.NullOr(Schema.String), linkType: LinkType, - metadata: Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), createdAt: M.Generated(JsonDate), updatedAt: M.Generated(JsonDate), }) {} diff --git a/packages/domain/src/models/message-model.ts b/packages/domain/src/models/message-model.ts index fbd1b9263..b30807006 100644 --- a/packages/domain/src/models/message-model.ts +++ b/packages/domain/src/models/message-model.ts @@ -30,4 +30,7 @@ export const Update = Model.update * Excludes immutable relationship fields (channelId, replyToMessageId, threadChannelId) * to prevent users from moving messages between channels or fabricating conversation context. */ -export const JsonUpdate = Model.jsonUpdate.pipe(Schema.pick("content", "embeds"), Schema.partial) +export const JsonUpdate = Schema.Struct({ + content: Schema.optionalKey((Model.jsonUpdate as any).fields.content), + embeds: Schema.optionalKey((Model.jsonUpdate as any).fields.embeds), +}) diff --git a/packages/domain/src/models/notification-model.ts b/packages/domain/src/models/notification-model.ts index 70f93f6cd..5d1be4bc4 100644 --- a/packages/domain/src/models/notification-model.ts +++ b/packages/domain/src/models/notification-model.ts @@ -6,9 +6,9 @@ import { JsonDate } from "./utils" export class Model extends M.Class("Notification")({ id: M.Generated(NotificationId), memberId: OrganizationMemberId, - targetedResourceId: Schema.NullOr(Schema.UUID), + targetedResourceId: Schema.NullOr(Schema.String.check(Schema.isUUID())), targetedResourceType: Schema.NullOr(Schema.String), - resourceId: Schema.NullOr(Schema.UUID), + resourceId: Schema.NullOr(Schema.String.check(Schema.isUUID())), resourceType: Schema.NullOr(Schema.String), createdAt: M.Generated(JsonDate), readAt: Schema.NullOr(JsonDate), diff --git a/packages/domain/src/models/organization-model.ts b/packages/domain/src/models/organization-model.ts index b83018c2f..76bf8fd54 100644 --- a/packages/domain/src/models/organization-model.ts +++ b/packages/domain/src/models/organization-model.ts @@ -9,10 +9,7 @@ export class Model extends M.Class("Organization")({ slug: Schema.NullOr(Schema.String), logoUrl: Schema.NullOr(Schema.String), settings: Schema.NullOr( - Schema.Record({ - key: Schema.String, - value: Schema.Unknown, - }), + Schema.Record(Schema.String, Schema.Unknown), ), isPublic: Schema.Boolean, ...baseFields, diff --git a/packages/domain/src/models/theme-model.ts b/packages/domain/src/models/theme-model.ts index 760f01adb..18415cc2f 100644 --- a/packages/domain/src/models/theme-model.ts +++ b/packages/domain/src/models/theme-model.ts @@ -24,11 +24,9 @@ export type RadiusPreset = Schema.Schema.Type /** * Hex color string branded type (e.g., "#6938EF") */ -export const HexColor = Schema.String.pipe( - Schema.pattern(/^#[0-9A-Fa-f]{6}$/), - Schema.brand("HexColor"), - Schema.annotate({ message: () => "Must be a valid hex color (#RRGGBB)" }), -) +export const HexColor = Schema.String.check( + Schema.isPattern(/^#[0-9A-Fa-f]{6}$/, { message: "Must be a valid hex color (#RRGGBB)" }), +).pipe(Schema.brand("HexColor")) export type HexColor = Schema.Schema.Type /** diff --git a/packages/domain/src/models/user-model.ts b/packages/domain/src/models/user-model.ts index 3d4b92c29..d35bfee2a 100644 --- a/packages/domain/src/models/user-model.ts +++ b/packages/domain/src/models/user-model.ts @@ -10,10 +10,9 @@ export type UserType = Schema.Schema.Type /** * Time in HH:MM format (00:00 - 23:59) */ -export const TimeString = Schema.String.pipe( - Schema.pattern(/^([01]\d|2[0-3]):([0-5]\d)$/), - Schema.brand("TimeString"), -) +export const TimeString = Schema.String.check( + Schema.isPattern(/^([01]\d|2[0-3]):([0-5]\d)$/), +).pipe(Schema.brand("TimeString")) export type TimeString = Schema.Schema.Type /** @@ -34,7 +33,7 @@ export class Model extends M.Class("User")({ email: Schema.String, firstName: Schema.String, lastName: Schema.String, - avatarUrl: Schema.NullishOr(Schema.NonEmptyTrimmedString), + avatarUrl: Schema.NullishOr(Schema.NonEmptyString), userType: UserType, settings: Schema.NullOr(UserSettingsSchema), isOnboarded: Schema.Boolean, diff --git a/packages/domain/src/rpc/channel-members.ts b/packages/domain/src/rpc/channel-members.ts index 776dc7fdb..c1c86f177 100644 --- a/packages/domain/src/rpc/channel-members.ts +++ b/packages/domain/src/rpc/channel-members.ts @@ -102,7 +102,7 @@ export class ChannelMemberRpcs extends RpcGroup.make( Rpc.make("channelMember.update", { payload: Schema.Struct({ id: ChannelMemberId, - }).pipe(Schema.extend(Schema.partial(ChannelMember.Model.jsonUpdate))), + }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(ChannelMember.Model.jsonUpdate as any).fields }) as any), success: ChannelMemberResponse, error: Schema.Union([ChannelMemberNotFoundError, UnauthorizedError, InternalServerError]), }) diff --git a/packages/domain/src/rpc/channel-sections.ts b/packages/domain/src/rpc/channel-sections.ts index 028043de4..184b32b29 100644 --- a/packages/domain/src/rpc/channel-sections.ts +++ b/packages/domain/src/rpc/channel-sections.ts @@ -70,7 +70,7 @@ export class ChannelSectionRpcs extends RpcGroup.make( Rpc.make("channelSection.update", { payload: Schema.Struct({ id: ChannelSectionId, - }).pipe(Schema.extend(Schema.partial(ChannelSection.Model.jsonUpdate))), + }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(ChannelSection.Model.jsonUpdate as any).fields }) as any), success: ChannelSectionResponse, error: Schema.Union([ChannelSectionNotFoundError, UnauthorizedError, InternalServerError]), }) diff --git a/packages/domain/src/rpc/channels.ts b/packages/domain/src/rpc/channels.ts index c08fc19ca..d797a5dd5 100644 --- a/packages/domain/src/rpc/channels.ts +++ b/packages/domain/src/rpc/channels.ts @@ -48,7 +48,7 @@ export class CreateDmChannelRequest extends Schema.Class participantIds: Schema.Array(UserId), type: Schema.Literals(["direct", "single"]), name: Schema.optional(Schema.String), - organizationId: Schema.UUID, + organizationId: Schema.String.check(Schema.isUUID()), }) {} /** @@ -68,12 +68,10 @@ export class CreateThreadRequest extends Schema.Class("Crea * Uses jsonCreate which includes optional id for optimistic updates. * Extended with addAllMembers option to auto-add all organization members. */ -export const CreateChannelRequest = Schema.extend( - Channel.Model.jsonCreate, - Schema.Struct({ - addAllMembers: Schema.optional(Schema.Boolean), - }), -) +export const CreateChannelRequest = Schema.Struct({ + ...(Channel.Model.jsonCreate as any).fields, + addAllMembers: Schema.optional(Schema.Boolean), +}) export class ChannelRpcs extends RpcGroup.make( /** @@ -111,7 +109,7 @@ export class ChannelRpcs extends RpcGroup.make( Rpc.make("channel.update", { payload: Schema.Struct({ id: ChannelId, - }).pipe(Schema.extend(Schema.partial(Channel.Model.jsonUpdate))), + }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(Channel.Model.jsonUpdate as any).fields }) as any), success: ChannelResponse, error: Schema.Union([ChannelNotFoundError, UnauthorizedError, InternalServerError]), }) diff --git a/packages/domain/src/rpc/chat-sync.ts b/packages/domain/src/rpc/chat-sync.ts index 6c55744dd..63cd5af63 100644 --- a/packages/domain/src/rpc/chat-sync.ts +++ b/packages/domain/src/rpc/chat-sync.ts @@ -88,8 +88,8 @@ export class ChatSyncRpcs extends RpcGroup.make( externalWorkspaceId: Schema.String, externalWorkspaceName: Schema.optional(Schema.String), integrationConnectionId: Schema.optional(IntegrationConnectionId), - settings: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), - metadata: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + settings: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }), success: ChatSyncConnectionResponse, error: Schema.Union([ @@ -131,7 +131,7 @@ export class ChatSyncRpcs extends RpcGroup.make( externalChannelId: ExternalChannelId, externalChannelName: Schema.optional(Schema.String), direction: Schema.optional(ChatSyncChannelLink.ChatSyncDirection), - settings: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + settings: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }), success: ChatSyncChannelLinkResponse, error: Schema.Union([ diff --git a/packages/domain/src/rpc/integration-requests.ts b/packages/domain/src/rpc/integration-requests.ts index cef61cead..9ab17deab 100644 --- a/packages/domain/src/rpc/integration-requests.ts +++ b/packages/domain/src/rpc/integration-requests.ts @@ -24,7 +24,7 @@ export class CreateIntegrationRequestPayload extends Schema.Class Schema.Struct({ ...s.fields, ...(Message.JsonUpdate as any).fields }) as any), success: MessageResponse, error: Schema.Union([ MessageNotFoundError, diff --git a/packages/domain/src/rpc/organizations.ts b/packages/domain/src/rpc/organizations.ts index bc2a6a697..5003aab69 100644 --- a/packages/domain/src/rpc/organizations.ts +++ b/packages/domain/src/rpc/organizations.ts @@ -80,7 +80,7 @@ export class OrganizationRpcs extends RpcGroup.make( Rpc.make("organization.update", { payload: Schema.Struct({ id: OrganizationId, - }).pipe(Schema.extend(Schema.partial(Organization.Model.jsonUpdate))), + }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(Organization.Model.jsonUpdate as any).fields }) as any), success: OrganizationResponse, error: Schema.Union([ OrganizationNotFoundError, diff --git a/packages/domain/src/rpc/typing-indicators.ts b/packages/domain/src/rpc/typing-indicators.ts index f5ee85bf4..1c814132a 100644 --- a/packages/domain/src/rpc/typing-indicators.ts +++ b/packages/domain/src/rpc/typing-indicators.ts @@ -36,8 +36,8 @@ export class TypingIndicatorNotFoundError extends Schema.TaggedErrorClass( "CreateTypingIndicatorPayload", )({ - channelId: Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelId")), - memberId: Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelMemberId")), + channelId: Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/ChannelId")), + memberId: Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/ChannelMemberId")), lastTyped: Schema.optional(Schema.Number), }) {} diff --git a/packages/domain/src/rpc/users.ts b/packages/domain/src/rpc/users.ts index 9fc7e9df3..c6da10c78 100644 --- a/packages/domain/src/rpc/users.ts +++ b/packages/domain/src/rpc/users.ts @@ -58,7 +58,7 @@ export class UserRpcs extends RpcGroup.make( Rpc.make("user.update", { payload: Schema.Struct({ id: UserId, - }).pipe(Schema.extend(Schema.partial(User.Model.jsonUpdate))), + }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(User.Model.jsonUpdate as any).fields }) as any), success: UserResponse, error: Schema.Union([UserNotFoundError, UnauthorizedError, InternalServerError]), }) diff --git a/packages/effect-bun/src/Redis.ts b/packages/effect-bun/src/Redis.ts index 5645d93f0..46a0d5300 100644 --- a/packages/effect-bun/src/Redis.ts +++ b/packages/effect-bun/src/Redis.ts @@ -239,9 +239,9 @@ export class Redis extends ServiceMap.Service - new RedisError({ + Effect.fail(new RedisError({ message: `Redis connection timed out after 10s (url: ${sanitizeRedisUrl(url)})`, - }), + })), }), ) @@ -273,9 +273,9 @@ export class Redis extends ServiceMap.Service - new RedisError({ + Effect.fail(new RedisError({ message: `Redis connection timed out after 10s (url: ${sanitizeRedisUrl(url)})`, - }), + })), }), ) diff --git a/packages/effect-bun/src/Telemetry.ts b/packages/effect-bun/src/Telemetry.ts index 8efcf92d1..23757a0cf 100644 --- a/packages/effect-bun/src/Telemetry.ts +++ b/packages/effect-bun/src/Telemetry.ts @@ -2,7 +2,7 @@ import { BunSocket } from "@effect/platform-bun" import { Config, Effect, Layer } from "effect" import { DevTools } from "effect/unstable/devtools" import { FetchHttpClient } from "effect/unstable/http" -import { Otlp } from "effect/unstable/observability" +import { Otlp, OtlpSerialization } from "effect/unstable/observability" /** * Create an OpenTelemetry tracing layer with a specific service name. @@ -61,6 +61,9 @@ export const createTracingLayer = (otelServiceName: string) => "deployment.commit_sha": commitSha, }, }, - }).pipe(Layer.provide(FetchHttpClient.layer)) + }).pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide(OtlpSerialization.layerJson), + ) }), ) diff --git a/packages/effect-bun/src/persistence/redis-backing.ts b/packages/effect-bun/src/persistence/redis-backing.ts index 8d7563546..c27ab42ce 100644 --- a/packages/effect-bun/src/persistence/redis-backing.ts +++ b/packages/effect-bun/src/persistence/redis-backing.ts @@ -1,8 +1,14 @@ import { Persistence } from "effect/unstable/persistence" -import { Duration, Effect, Layer, Option } from "effect" +import { Duration, Effect, Layer } from "effect" import { identity } from "effect/Function" import { Redis } from "../Redis.js" +const makePersistenceError = (method: string, error: unknown) => + new Persistence.PersistenceError({ + message: `Persistence error in ${method}`, + cause: error, + }) + /** * Create a BackingPersistence using @hazel/effect-bun Redis service. * This is the core implementation that bridges Redis to Effect's Persistence system. @@ -11,16 +17,15 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { const redis = yield* Redis return Persistence.BackingPersistence.of({ - [Persistence.BackingPersistence]: Persistence.BackingPersistence, make: (prefix) => Effect.sync(() => { const prefixed = (key: string) => `${prefix}:${key}` - const parse = (method: string) => (str: string | null) => { - if (str === null) return Effect.succeedNone + const parse = (method: string) => (str: string | null): Effect.Effect => { + if (str === null) return Effect.succeed(undefined) return Effect.try({ - try: () => Option.some(JSON.parse(str)), - catch: (error) => Persistence.PersistenceError.make(method, error), + try: () => JSON.parse(str) as object, + catch: (error) => makePersistenceError(method, error), }) } @@ -31,44 +36,45 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .get(prefixed(key)) .pipe( Effect.mapError((error) => - Persistence.PersistenceError.make("get", error), + makePersistenceError("get", error), ), ), parse("get"), ), getMany: (keys) => - Effect.flatMap( - redis - .send<(string | null)[]>("MGET", keys.map(prefixed)) - .pipe( - Effect.mapError((error) => - Persistence.PersistenceError.make("getMany", error), - ), + redis + .send<(string | null)[]>("MGET", keys.map(prefixed)) + .pipe( + Effect.mapError((error) => + makePersistenceError("getMany", error), ), - Effect.forEach(parse("getMany")), - ), + Effect.flatMap((results) => + Effect.forEach(results, parse("getMany")), + ), + Effect.map((results) => results as any), + ), set: (key, value, ttl) => Effect.gen(function* () { const serialized = yield* Effect.try({ try: () => JSON.stringify(value), - catch: (error) => Persistence.PersistenceError.make("set", error), + catch: (error) => makePersistenceError("set", error), }) const pkey = prefixed(key) - if (Option.isSome(ttl)) { + if (ttl !== undefined) { // Atomic SET with PX (milliseconds) - sets value and TTL in single command yield* redis .send("SET", [ pkey, serialized, "PX", - String(Duration.toMillis(ttl.value)), + String(Duration.toMillis(ttl)), ]) .pipe( Effect.mapError((error) => - Persistence.PersistenceError.make("set", error), + makePersistenceError("set", error), ), ) } else { @@ -76,7 +82,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .set(pkey, serialized) .pipe( Effect.mapError((error) => - Persistence.PersistenceError.make("set", error), + makePersistenceError("set", error), ), ) } @@ -87,17 +93,17 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { for (const [key, value, ttl] of entries) { const pkey = prefixed(key) const serialized = JSON.stringify(value) - if (Option.isSome(ttl)) { + if (ttl !== undefined) { yield* redis .send("SET", [ pkey, serialized, "PX", - String(Duration.toMillis(ttl.value)), + String(Duration.toMillis(ttl)), ]) .pipe( Effect.mapError((error) => - Persistence.PersistenceError.make("setMany", error), + makePersistenceError("setMany", error), ), ) } else { @@ -105,7 +111,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .set(pkey, serialized) .pipe( Effect.mapError((error) => - Persistence.PersistenceError.make("setMany", error), + makePersistenceError("setMany", error), ), ) } @@ -117,7 +123,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .del(prefixed(key)) .pipe( Effect.mapError((error) => - Persistence.PersistenceError.make("remove", error), + makePersistenceError("remove", error), ), ), @@ -126,7 +132,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .send("KEYS", [`${prefix}:*`]) .pipe( Effect.mapError((error) => - Persistence.PersistenceError.make("clear", error), + makePersistenceError("clear", error), ), ) if (keys.length > 0) { @@ -134,7 +140,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { .send("DEL", keys) .pipe( Effect.mapError((error) => - Persistence.PersistenceError.make("clear", error), + makePersistenceError("clear", error), ), ) } @@ -155,16 +161,16 @@ export const RedisBackingPersistenceLive = Layer.effect( ) /** - * Layer providing ResultPersistence using Redis backing. + * Layer providing Persistence using Redis backing. * Requires: Redis - * Provides: Persistence.ResultPersistence + * Provides: Persistence.Persistence */ -export const RedisResultPersistenceLive = Persistence.layerResult.pipe( +export const RedisResultPersistenceLive = Persistence.layer.pipe( Layer.provide(RedisBackingPersistenceLive), ) /** * In-memory persistence layer for testing or fallback. - * Provides: Persistence.ResultPersistence + * Provides: Persistence.Persistence */ export const MemoryResultPersistenceLive = Persistence.layerMemory From 6906d76a74ba18bb7df16cbe80708ea00a377485 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 00:42:22 +0100 Subject: [PATCH 08/34] fix more stuff --- .../src/repositories/message-outbox-repo.ts | 2 +- packages/db/src/services/drizzle-effect.ts | 2 +- packages/domain/src/http/api.ts | 2 +- packages/domain/src/http/chat-sync.ts | 12 +-- packages/domain/src/http/incoming-webhooks.ts | 6 +- .../domain/src/http/integration-resources.ts | 10 +-- packages/domain/src/http/integrations.ts | 10 +-- packages/domain/src/http/presence.ts | 2 +- packages/domain/src/http/webhooks.ts | 4 +- packages/domain/src/models/utils.ts | 25 +++--- packages/domain/src/policy/index.ts | 4 +- packages/domain/src/rpc/bots.ts | 8 +- packages/domain/src/rpc/channel-webhooks.ts | 8 +- .../domain/src/scopes/current-bot-scopes.ts | 9 ++- .../domain/src/scopes/current-rpc-scopes.ts | 14 ++-- packages/domain/src/scopes/scope-map.ts | 12 +-- packages/domain/src/scopes/validate-scopes.ts | 8 +- packages/rivet-effect/src/action.ts | 5 +- packages/rivet-effect/src/actor.ts | 9 +-- packages/rivet-effect/src/lifecycle.ts | 41 +++++----- packages/rivet-effect/src/runtime.test.ts | 12 +-- packages/rivet-effect/src/runtime.ts | 14 ++-- packages/schema/src/ids.ts | 76 +++++++++---------- 23 files changed, 146 insertions(+), 149 deletions(-) diff --git a/packages/backend-core/src/repositories/message-outbox-repo.ts b/packages/backend-core/src/repositories/message-outbox-repo.ts index bdb67963c..667d3977d 100644 --- a/packages/backend-core/src/repositories/message-outbox-repo.ts +++ b/packages/backend-core/src/repositories/message-outbox-repo.ts @@ -84,7 +84,7 @@ export type MessageOutboxEventRecord = typeof schema.messageOutboxEventsTable.$i const InsertMessageOutboxEventSchema = Schema.Struct({ eventType: MessageOutboxEventType, - aggregateId: Schema.UUID, + aggregateId: Schema.String.check(Schema.isUUID()), channelId: ChannelId, payload: MessageOutboxEventPayloadSchema, }) diff --git a/packages/db/src/services/drizzle-effect.ts b/packages/db/src/services/drizzle-effect.ts index fc25c0beb..7e89fe8c1 100644 --- a/packages/db/src/services/drizzle-effect.ts +++ b/packages/db/src/services/drizzle-effect.ts @@ -260,7 +260,7 @@ function mapColumnToSchema(column: Drizzle.Column): Schema.Schema { if (!type) { if (Drizzle.is(column, DrizzlePg.PgUUID)) { - type = Schema.UUID + type = Schema.String.check(Schema.isUUID()) } else if (column.dataType === "custom") { type = Schema.Any } else if (column.dataType === "json") { diff --git a/packages/domain/src/http/api.ts b/packages/domain/src/http/api.ts index 39f2ce509..6f2a80e5d 100644 --- a/packages/domain/src/http/api.ts +++ b/packages/domain/src/http/api.ts @@ -32,7 +32,7 @@ export class HazelApi extends HttpApi.make("HazelApp") .add(WebhookGroup) .add(MockDataGroup) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Hazel Chat API", description: "API for the Hazel chat application", version: "1.0.0", diff --git a/packages/domain/src/http/chat-sync.ts b/packages/domain/src/http/chat-sync.ts index 77551e548..0e9cd9d93 100644 --- a/packages/domain/src/http/chat-sync.ts +++ b/packages/domain/src/http/chat-sync.ts @@ -114,7 +114,7 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") error: [ChatSyncConnectionExistsError, ChatSyncIntegrationNotConnectedError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Create Chat Sync Connection", description: "Create a provider-agnostic chat sync connection (Discord, Slack, etc.)", summary: "Create sync connection", @@ -129,7 +129,7 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") error: [UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "List Chat Sync Connections", description: "List chat sync connections for an organization", summary: "List sync connections", @@ -144,7 +144,7 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") error: [ChatSyncConnectionNotFoundError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Delete Chat Sync Connection", description: "Soft-delete a chat sync connection", summary: "Delete sync connection", @@ -160,7 +160,7 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") error: [ChatSyncConnectionNotFoundError, ChatSyncChannelLinkExistsError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Create Chat Sync Channel Link", description: "Link a Hazel channel to an external provider channel", summary: "Create channel link", @@ -175,7 +175,7 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") error: [ChatSyncConnectionNotFoundError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "List Chat Sync Channel Links", description: "List channel links for a sync connection", summary: "List channel links", @@ -190,7 +190,7 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") error: [ChatSyncChannelLinkNotFoundError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Delete Chat Sync Channel Link", description: "Soft-delete a chat sync channel link", summary: "Delete channel link", diff --git a/packages/domain/src/http/incoming-webhooks.ts b/packages/domain/src/http/incoming-webhooks.ts index 95e801dbc..5f8424fb2 100644 --- a/packages/domain/src/http/incoming-webhooks.ts +++ b/packages/domain/src/http/incoming-webhooks.ts @@ -139,7 +139,7 @@ export class IncomingWebhookGroup extends HttpApiGroup.make("incoming-webhooks") error: [WebhookNotFoundError, WebhookDisabledError, InvalidWebhookTokenError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Execute Incoming Webhook", description: "Post a message to a channel via webhook. Supports plain text content and Discord-style embeds.", @@ -159,7 +159,7 @@ export class IncomingWebhookGroup extends HttpApiGroup.make("incoming-webhooks") error: [WebhookNotFoundError, WebhookDisabledError, InvalidWebhookTokenError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Execute OpenStatus Webhook", description: "Receive status alerts from OpenStatus and post them as rich embeds to a channel.", @@ -179,7 +179,7 @@ export class IncomingWebhookGroup extends HttpApiGroup.make("incoming-webhooks") error: [WebhookNotFoundError, WebhookDisabledError, InvalidWebhookTokenError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Execute Railway Webhook", description: "Receive deployment and alert events from Railway and post them as rich embeds to a channel.", diff --git a/packages/domain/src/http/integration-resources.ts b/packages/domain/src/http/integration-resources.ts index 09fae9f23..ce9240f38 100644 --- a/packages/domain/src/http/integration-resources.ts +++ b/packages/domain/src/http/integration-resources.ts @@ -167,7 +167,7 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, ResourceNotFoundError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Fetch Linear Issue", description: "Fetch Linear issue details for embedding in chat messages", summary: "Get Linear issue preview data", @@ -183,7 +183,7 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, ResourceNotFoundError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Fetch GitHub PR", description: "Fetch GitHub pull request details for embedding in chat messages", summary: "Get GitHub PR preview data", @@ -202,7 +202,7 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res error: [IntegrationNotConnectedForPreviewError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Get GitHub Repositories", description: "List repositories accessible to the GitHub App installation", summary: "List GitHub repositories", @@ -217,7 +217,7 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Get Discord Guilds", description: "List Discord guilds visible to the connected Discord account", summary: "List Discord guilds", @@ -235,7 +235,7 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Get Discord Guild Channels", description: "List message-capable channels in a Discord guild using the bot token", summary: "List Discord guild channels", diff --git a/packages/domain/src/http/integrations.ts b/packages/domain/src/http/integrations.ts index f0219644c..ecc52cda3 100644 --- a/packages/domain/src/http/integrations.ts +++ b/packages/domain/src/http/integrations.ts @@ -79,7 +79,7 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") }) .middleware(CurrentUser.Authorization) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Get OAuth Authorization URL", description: "Returns the OAuth authorization URL for the provider. The frontend should redirect the user to this URL. Sets a session cookie to preserve context for the callback.", @@ -109,7 +109,7 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") error: [InvalidOAuthStateError, UnsupportedProviderError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "OAuth Callback", description: "Handle OAuth callback from integration provider", summary: "Process OAuth callback", @@ -132,7 +132,7 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") }) .middleware(CurrentUser.Authorization) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Get Connection Status", description: "Check the connection status for a provider", summary: "Get integration status", @@ -153,7 +153,7 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") }) .middleware(CurrentUser.Authorization) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Connect via API Key", description: "Connect an integration using an API key/token instead of OAuth. Validates the credentials against the provider and stores the connection.", @@ -176,7 +176,7 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") error: [IntegrationNotConnectedError, UnsupportedProviderError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Disconnect Integration", description: "Disconnect an integration and revoke tokens", summary: "Disconnect provider", diff --git a/packages/domain/src/http/presence.ts b/packages/domain/src/http/presence.ts index 5b417c1c5..c50c2bcf0 100644 --- a/packages/domain/src/http/presence.ts +++ b/packages/domain/src/http/presence.ts @@ -22,7 +22,7 @@ export class PresencePublicGroup extends HttpApiGroup.make("presencePublic") error: InternalServerError, }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "Mark User Offline", description: "Mark a user as offline when they close their tab (no auth required)", summary: "Mark offline", diff --git a/packages/domain/src/http/webhooks.ts b/packages/domain/src/http/webhooks.ts index bc72fd5fc..ef229a346 100644 --- a/packages/domain/src/http/webhooks.ts +++ b/packages/domain/src/http/webhooks.ts @@ -50,7 +50,7 @@ export class WebhookGroup extends HttpApiGroup.make("webhooks") error: [InvalidWebhookSignature, InternalServerError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "WorkOS Webhook", description: "Receive and process WorkOS webhook events", summary: "Process WorkOS webhook events", @@ -65,7 +65,7 @@ export class WebhookGroup extends HttpApiGroup.make("webhooks") error: [InvalidGitHubWebhookSignature, InternalServerError, WorkflowInitializationError], }) .annotateMerge( - OpenApi.annotations({ + OpenApi.annotate({ title: "GitHub App Webhook", description: "Receive and process GitHub App webhook events", summary: "Process GitHub App webhook events", diff --git a/packages/domain/src/models/utils.ts b/packages/domain/src/models/utils.ts index 4e22324bb..2d1981d8a 100644 --- a/packages/domain/src/models/utils.ts +++ b/packages/domain/src/models/utils.ts @@ -5,7 +5,7 @@ import * as Effect from "effect/Effect" import * as Schema from "effect/Schema" import * as SchemaIssue from "effect/SchemaIssue" -const { Class, Field, FieldExcept, FieldOnly, Struct, Union, extract, fieldEvolve, fieldFromKey } = +const { Class, Field, FieldExcept, FieldOnly, Struct, Union, extract, fieldEvolve } = VariantSchema.make({ variants: ["select", "insert", "update", "json", "jsonCreate", "jsonUpdate"], defaultVariant: "select", @@ -58,13 +58,12 @@ export { Field, fieldEvolve, FieldExcept, - fieldFromKey, FieldOnly, Struct, Union, } -export const fields: >(self: A) => A[VariantSchema.TypeId] = +export const fields: >(self: A) => A[typeof VariantSchema.TypeId] = VariantSchema.fields export const Override: (value: A) => A & Brand<"Override"> = VariantSchema.Override @@ -190,17 +189,15 @@ export interface Date extends Schema.decodeTo< /** A DateTime.Utc serialized as ISO date string (YYYY-MM-DD). */ export const Date: Date = Schema.String.pipe( - Schema.decodeTo(Schema.DateTimeUtc, { - decode: (s, _, ast) => - DateTime.make(s).pipe( - (opt) => { - if (opt._tag === "Some") { - return Effect.succeed(DateTime.removeTime(opt.value)) - } - return Effect.fail(new SchemaIssue.InvalidValue(ast, s)) - }, - ), - encode: (dt) => Effect.succeed(DateTime.formatIsoDate(dt)), + Schema.decode({ + decode: (s: string) => { + const opt = DateTime.make(s) + if (opt._tag === "Some") { + return Effect.succeed(DateTime.removeTime(opt.value)) + } + return Effect.fail("Invalid date format") + }, + encode: (dt: DateTime.Utc) => Effect.succeed(DateTime.formatIsoDate(dt)), }), ) as any diff --git a/packages/domain/src/policy/index.ts b/packages/domain/src/policy/index.ts index 9d9570f63..cfd6cefb1 100644 --- a/packages/domain/src/policy/index.ts +++ b/packages/domain/src/policy/index.ts @@ -7,7 +7,7 @@ export const policy = ( action: Action, f: (actor: typeof CurrentUser.Schema.Type) => Effect.Effect, ): Effect.Effect => - Effect.flatMap(CurrentUser.Context, (actor) => + CurrentUser.Context.use((actor: typeof CurrentUser.Schema.Type) => Effect.flatMap(f(actor), (can) => can ? Effect.void @@ -18,4 +18,4 @@ export const policy = ( }), ), ), - ) + ) as Effect.Effect diff --git a/packages/domain/src/rpc/bots.ts b/packages/domain/src/rpc/bots.ts index 4378f4cd0..6aff2c04e 100644 --- a/packages/domain/src/rpc/bots.ts +++ b/packages/domain/src/rpc/bots.ts @@ -113,8 +113,8 @@ export class BotRpcs extends RpcGroup.make( */ Rpc.make("bot.create", { payload: Schema.Struct({ - name: Schema.String.pipe(Schema.isMinLength(1), Schema.isMaxLength(100)), - description: Schema.optional(Schema.String.pipe(Schema.isMaxLength(500))), + name: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(100)), + description: Schema.optional(Schema.String.check(Schema.isMaxLength(500))), webhookUrl: Schema.optional(Schema.String), scopes: Schema.Array(ApiScope), isPublic: Schema.optional(Schema.Boolean), @@ -172,8 +172,8 @@ export class BotRpcs extends RpcGroup.make( Rpc.make("bot.update", { payload: Schema.Struct({ id: BotId, - name: Schema.optional(Schema.String.pipe(Schema.isMinLength(1), Schema.isMaxLength(100))), - description: Schema.optional(Schema.NullOr(Schema.String.pipe(Schema.isMaxLength(500)))), + name: Schema.optional(Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(100))), + description: Schema.optional(Schema.NullOr(Schema.String.check(Schema.isMaxLength(500)))), webhookUrl: Schema.optional(Schema.NullOr(Schema.String)), scopes: Schema.optional(Schema.Array(ApiScope)), isPublic: Schema.optional(Schema.Boolean), diff --git a/packages/domain/src/rpc/channel-webhooks.ts b/packages/domain/src/rpc/channel-webhooks.ts index 62ce45798..12ebf3e08 100644 --- a/packages/domain/src/rpc/channel-webhooks.ts +++ b/packages/domain/src/rpc/channel-webhooks.ts @@ -77,8 +77,8 @@ export class ChannelWebhookRpcs extends RpcGroup.make( Rpc.make("channelWebhook.create", { payload: Schema.Struct({ channelId: ChannelId, - name: Schema.String.pipe(Schema.isMinLength(1), Schema.isMaxLength(100)), - description: Schema.optional(Schema.String.pipe(Schema.isMaxLength(500))), + name: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(100)), + description: Schema.optional(Schema.String.check(Schema.isMaxLength(500))), avatarUrl: Schema.optional(AvatarUrl), /** When set, uses a global integration bot user instead of creating a unique webhook bot */ integrationProvider: Schema.optional(Schema.Literals(["openstatus", "railway"])), @@ -120,8 +120,8 @@ export class ChannelWebhookRpcs extends RpcGroup.make( Rpc.make("channelWebhook.update", { payload: Schema.Struct({ id: ChannelWebhookId, - name: Schema.optional(Schema.String.pipe(Schema.isMinLength(1), Schema.isMaxLength(100))), - description: Schema.optional(Schema.NullOr(Schema.String.pipe(Schema.isMaxLength(500)))), + name: Schema.optional(Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(100))), + description: Schema.optional(Schema.NullOr(Schema.String.check(Schema.isMaxLength(500)))), avatarUrl: Schema.optional(Schema.NullOr(AvatarUrl)), isEnabled: Schema.optional(Schema.Boolean), }), diff --git a/packages/domain/src/scopes/current-bot-scopes.ts b/packages/domain/src/scopes/current-bot-scopes.ts index 03e7da439..096e1f1cb 100644 --- a/packages/domain/src/scopes/current-bot-scopes.ts +++ b/packages/domain/src/scopes/current-bot-scopes.ts @@ -1,8 +1,8 @@ -import { FiberRef, Option } from "effect" +import { ServiceMap, Option } from "effect" import type { ApiScope } from "./api-scope" /** - * FiberRef holding the authenticated bot's granted API scopes. + * Service holding the authenticated bot's granted API scopes. * * Set by the auth middleware when the actor is a bot. * When Option.some, OrgResolver uses these scopes instead of role-based @@ -11,4 +11,7 @@ import type { ApiScope } from "./api-scope" * When Option.none (default), OrgResolver falls back to role-based scopes * for normal (human) users. */ -export const CurrentBotScopes = FiberRef.unsafeMake>>(Option.none()) +export class CurrentBotScopes extends ServiceMap.Service>>()( + "CurrentBotScopes", + { defaultValue: Option.none() }, +) {} diff --git a/packages/domain/src/scopes/current-rpc-scopes.ts b/packages/domain/src/scopes/current-rpc-scopes.ts index 30397f972..9161453e3 100644 --- a/packages/domain/src/scopes/current-rpc-scopes.ts +++ b/packages/domain/src/scopes/current-rpc-scopes.ts @@ -1,15 +1,15 @@ -import { FiberRef } from "effect" +import { ServiceMap } from "effect" import type { ApiScope } from "./api-scope" /** - * FiberRef holding the required scopes for the currently executing RPC. + * Service holding the required scopes for the currently executing RPC. * - * Populated by the ScopeInjectionMiddleware (via Effect.locally) from the + * Populated by the ScopeInjectionMiddleware from the * RPC's RequiredScopes annotation. Policy utilities read from this instead * of accepting hardcoded scope strings, ensuring annotation and enforcement * always match. - * - * Uses FiberRef (not Context.Tag) so it doesn't leak into the R type of - * Effect.Service layers. */ -export const CurrentRpcScopes = FiberRef.unsafeMake>([]) +export class CurrentRpcScopes extends ServiceMap.Service>()( + "CurrentRpcScopes", + { defaultValue: [] as ReadonlyArray }, +) {} diff --git a/packages/domain/src/scopes/scope-map.ts b/packages/domain/src/scopes/scope-map.ts index 33b869a39..0e5f21b53 100644 --- a/packages/domain/src/scopes/scope-map.ts +++ b/packages/domain/src/scopes/scope-map.ts @@ -1,4 +1,4 @@ -import { Context, Option } from "effect" +import { ServiceMap, Option } from "effect" import type { ApiScope } from "./api-scope" import { RequiredScopes } from "./required-scopes" @@ -10,17 +10,17 @@ export type ScopeMap = Record> /** * Extracts a ScopeMap from an RpcGroup's `requests` map. * - * Each entry in `requests` has an `annotations` field (a `Context.Context`) + * Each entry in `requests` has an `annotations` field (a `ServiceMap.ServiceMap`) * where we look up the `RequiredScopes` tag. */ export const scopeMapFromRpcGroup = ( - requests: ReadonlyMap }>, + requests: ReadonlyMap }>, ): ScopeMap => { const map: Record> = {} for (const [tag, rpc] of requests) { - const scopes = Context.getOption(rpc.annotations, RequiredScopes) - if (Option.isSome(scopes)) { - map[tag] = scopes.value + const scopes = ServiceMap.get(rpc.annotations, RequiredScopes) as ReadonlyArray | undefined + if (scopes) { + map[tag] = scopes } } return map diff --git a/packages/domain/src/scopes/validate-scopes.ts b/packages/domain/src/scopes/validate-scopes.ts index aadc0443e..e5e484647 100644 --- a/packages/domain/src/scopes/validate-scopes.ts +++ b/packages/domain/src/scopes/validate-scopes.ts @@ -1,4 +1,4 @@ -import { Context, Option } from "effect" +import { ServiceMap } from "effect" import { RequiredScopes } from "./required-scopes" /** @@ -6,13 +6,13 @@ import { RequiredScopes } from "./required-scopes" * Returns the list of RPC tags that are missing annotations. */ export const validateRpcGroupScopes = ( - requests: ReadonlyMap }>, + requests: ReadonlyMap }>, groupName: string, ): { valid: boolean; missing: string[] } => { const missing: string[] = [] for (const [tag, rpc] of requests) { - const scopes = Context.getOption(rpc.annotations, RequiredScopes) - if (Option.isNone(scopes)) { + const scopes = ServiceMap.get(rpc.annotations, RequiredScopes) as ReadonlyArray | undefined + if (!scopes) { missing.push(`${groupName}.${tag}`) } } diff --git a/packages/rivet-effect/src/action.ts b/packages/rivet-effect/src/action.ts index ef6fd025e..e0b0f5348 100644 --- a/packages/rivet-effect/src/action.ts +++ b/packages/rivet-effect/src/action.ts @@ -1,6 +1,5 @@ import { Effect } from "effect" import type { ActorContext, ActionContext } from "rivetkit" -import type { YieldWrap } from "effect/Utils" import { provideActorContext } from "./actor.ts" import { runPromise } from "./runtime.ts" @@ -47,11 +46,11 @@ export function effect< genFn: ( c: ActorContext, ...args: Args - ) => Generator>, AEff, never>, + ) => Generator, ): (c: ActionContext, ...args: Args) => AEff { return ((c, ...args) => { const gen = genFn(c, ...args) - const eff = Effect.gen>, AEff>(() => gen) + const eff = Effect.gen(() => gen) const withContext = provideActorContext(eff, c) return runPromise(withContext, c) }) as (c: ActionContext, ...args: Args) => AEff diff --git a/packages/rivet-effect/src/actor.ts b/packages/rivet-effect/src/actor.ts index ceda6a071..2c3653063 100644 --- a/packages/rivet-effect/src/actor.ts +++ b/packages/rivet-effect/src/actor.ts @@ -1,6 +1,5 @@ import { Cause, Effect, Exit, ServiceMap } from "effect" import type { ActorContext } from "rivetkit" -import type { YieldWrap } from "effect/Utils" import { StatePersistenceError } from "./errors.ts" import { runPromise, runPromiseExit } from "./runtime.ts" @@ -22,7 +21,7 @@ export const provideActorContext = ( context: unknown, ): Effect.Effect> => Effect.provideService( - effect as Effect.Effect, + make as Effect.Effect, RivetActorContext, context as AnyActorContext, ) as Effect.Effect> @@ -154,7 +153,7 @@ export const waitUntil = , ): Effect.Effect => Effect.sync(() => { - const promise = runPromiseExit(effect, c).then((exit) => { + const promise = runPromiseExit(make, c).then((exit) => { if (Exit.isFailure(exit)) { c.log.error({ msg: "waitUntil effect failed", @@ -181,11 +180,11 @@ export const destroy = ( export function effect( genFn: ( c: ActorContext, - ) => Generator>, AEff, never>, + ) => Generator, ): (c: ActorContext) => Promise { return (c) => { const gen = genFn(c) - const eff = Effect.gen>, AEff>(() => gen) + const eff = Effect.gen(() => gen) return runEffectOnActorContext(c, eff) } } diff --git a/packages/rivet-effect/src/lifecycle.ts b/packages/rivet-effect/src/lifecycle.ts index 94d3d72c4..7aed9780f 100644 --- a/packages/rivet-effect/src/lifecycle.ts +++ b/packages/rivet-effect/src/lifecycle.ts @@ -16,21 +16,20 @@ import type { WakeContext, WebSocketContext, } from "rivetkit" -import type { YieldWrap } from "effect/Utils" import { provideActorContext } from "./actor.ts" import { runPromise, runPromiseExit } from "./runtime.ts" -const runWithContext = (context: unknown, effect: Effect.Effect): Promise => - runPromise(provideActorContext(effect, context), context) +const runWithContext = (context: unknown, eff: Effect.Effect): Promise => + runPromise(provideActorContext(eff, context), context) const runWithContextExit = ( context: unknown, - make: Effect.Effect, -): Promise> => runPromiseExit(provideActorContext(effect, context), context) + eff: Effect.Effect, +): Promise> => runPromiseExit(provideActorContext(eff, context), context) const runGeneratorWithContext = ( context: unknown, - gen: Generator>, A, never>, + gen: Generator, ): Promise => runWithContext( context, @@ -38,7 +37,7 @@ const runGeneratorWithContext = ( ) const makeAsyncLifecycle = ( - genFn: (context: C, ...args: Args) => Generator>, AEff, never>, + genFn: (context: C, ...args: Args) => Generator, ) => { return (context: C, ...args: Args): Promise => runGeneratorWithContext(context, genFn(context, ...args)) @@ -49,7 +48,7 @@ export namespace OnCreate { genFn: ( c: CreateContext, input: TInput, - ) => Generator>, AEff, never>, + ) => Generator, ): ((c: CreateContext, input: TInput) => Promise) => makeAsyncLifecycle(genFn) } @@ -58,7 +57,7 @@ export namespace OnWake { export const effect = ( genFn: ( c: WakeContext, - ) => Generator>, AEff, never>, + ) => Generator, ): ((c: WakeContext) => Promise) => makeAsyncLifecycle(genFn) } @@ -67,7 +66,7 @@ export namespace OnDestroy { export const effect = ( genFn: ( c: DestroyContext, - ) => Generator>, AEff, never>, + ) => Generator, ): ((c: DestroyContext) => Promise) => makeAsyncLifecycle(genFn) } @@ -76,7 +75,7 @@ export namespace OnSleep { export const effect = ( genFn: ( c: SleepContext, - ) => Generator>, AEff, never>, + ) => Generator, ): ((c: SleepContext) => Promise) => makeAsyncLifecycle(genFn) } @@ -86,7 +85,7 @@ export namespace OnStateChange { genFn: ( c: StateChangeContext, newState: TState, - ) => Generator>, void, never>, + ) => Generator, ): ( c: StateChangeContext, newState: TState, @@ -112,7 +111,7 @@ export namespace OnBeforeConnect { genFn: ( c: BeforeConnectContext, params: TConnParams, - ) => Generator>, AEff, never>, + ) => Generator, ): ((c: BeforeConnectContext, params: TConnParams) => Promise) => makeAsyncLifecycle(genFn) } @@ -122,7 +121,7 @@ export namespace OnConnect { genFn: ( c: ConnectContext, conn: Conn, - ) => Generator>, AEff, never>, + ) => Generator, ): (( c: ConnectContext, conn: Conn, @@ -134,7 +133,7 @@ export namespace OnDisconnect { genFn: ( c: DisconnectContext, conn: Conn, - ) => Generator>, AEff, never>, + ) => Generator, ): (( c: DisconnectContext, conn: Conn, @@ -146,7 +145,7 @@ export namespace CreateConnState { genFn: ( c: CreateConnStateContext, params: TConnParams, - ) => Generator>, TConnState, never>, + ) => Generator, ): (( c: CreateConnStateContext, params: TConnParams, @@ -160,7 +159,7 @@ export namespace OnBeforeActionResponse { name: string, args: unknown[], output: Out, - ) => Generator>, Out, never>, + ) => Generator, ): (( c: BeforeActionResponseContext, name: string, @@ -174,7 +173,7 @@ export namespace CreateState { genFn: ( c: CreateContext, input: TInput, - ) => Generator>, TState, never>, + ) => Generator, ): ((c: CreateContext, input: TInput) => Promise) => makeAsyncLifecycle(genFn) } @@ -184,7 +183,7 @@ export namespace CreateVars { genFn: ( c: CreateVarsContext, driverCtx: unknown, - ) => Generator>, TVars, never>, + ) => Generator, ): ((c: CreateVarsContext, driverCtx: unknown) => Promise) => makeAsyncLifecycle(genFn) } @@ -194,7 +193,7 @@ export namespace OnRequest { genFn: ( c: RequestContext, request: Request, - ) => Generator>, Response, never>, + ) => Generator, ): (( c: RequestContext, request: Request, @@ -206,7 +205,7 @@ export namespace OnWebSocket { genFn: ( c: WebSocketContext, websocket: UniversalWebSocket, - ) => Generator>, AEff, never>, + ) => Generator, ): (( c: WebSocketContext, websocket: UniversalWebSocket, diff --git a/packages/rivet-effect/src/runtime.test.ts b/packages/rivet-effect/src/runtime.test.ts index f5e1c3b24..945ec052f 100644 --- a/packages/rivet-effect/src/runtime.test.ts +++ b/packages/rivet-effect/src/runtime.test.ts @@ -1,4 +1,4 @@ -import { Cause, Effect, Exit, Option } from "effect" +import { Cause, Effect, Exit, Result } from "effect" import { describe, expect, it, vi } from "@effect/vitest" import type { AnyManagedRuntime } from "./runtime.ts" import { runPromise, runPromiseExit, setManagedRuntime } from "./runtime.ts" @@ -22,11 +22,11 @@ describe("@hazel/rivet-effect runtime", () => { expect(Exit.isFailure(exit)).toBe(true) if (Exit.isFailure(exit)) { - const defect = Cause.dieOption(exit.cause) - expect(Option.isSome(defect)).toBe(true) - if (Option.isSome(defect)) { - expect((defect.value as any)?._tag).toBe("RuntimeExecutionError") - expect((defect.value as any)?.operation).toBe("runPromiseExit") + const defect = Cause.findDefect(exit.cause) + expect(Result.isSuccess(defect)).toBe(true) + if (Result.isSuccess(defect)) { + expect((defect.success as any)?._tag).toBe("RuntimeExecutionError") + expect((defect.success as any)?.operation).toBe("runPromiseExit") } } }) diff --git a/packages/rivet-effect/src/runtime.ts b/packages/rivet-effect/src/runtime.ts index 202af9764..e7a353a9f 100644 --- a/packages/rivet-effect/src/runtime.ts +++ b/packages/rivet-effect/src/runtime.ts @@ -1,4 +1,4 @@ -import { Cause, Effect, Exit, Layer, ManagedRuntime, Option } from "effect" +import { Cause, Effect, Exit, Layer, ManagedRuntime, Option, Result } from "effect" import { RuntimeExecutionError } from "./errors.ts" export type AnyManagedRuntime = ManagedRuntime.ManagedRuntime @@ -77,14 +77,14 @@ const runExitWithCurrentRuntime = (effect: Effect.Effect): Pr ) const throwFromCause = (cause: Cause.Cause): never => { - const failure = Cause.failureOption(cause) + const failure = Cause.findErrorOption(cause) if (Option.isSome(failure)) { throw failure.value } - const defect = Cause.dieOption(cause) - if (Option.isSome(defect)) { - throw defect.value instanceof Error ? defect.value : new Error(String(defect.value)) + const defect = Cause.findDefect(cause) + if (Result.isSuccess(defect)) { + throw defect.success instanceof Error ? defect.success : new Error(String(defect.success)) } throw new Error(Cause.pretty(cause)) @@ -104,8 +104,8 @@ export const runPromiseExit = ( ): Promise> => { const runtime = getManagedRuntime(context) const execution: Promise> = runtime - ? runtime.runPromiseExit(effect as Effect.Effect) - : runExitWithCurrentRuntime(effect as Effect.Effect) + ? runtime.runPromiseExit(make as Effect.Effect) + : runExitWithCurrentRuntime(make as Effect.Effect) return execution.catch((error) => Exit.die( diff --git a/packages/schema/src/ids.ts b/packages/schema/src/ids.ts index 993c651fd..21a93e673 100644 --- a/packages/schema/src/ids.ts +++ b/packages/schema/src/ids.ts @@ -1,12 +1,12 @@ import { Schema } from "effect" -export const ChannelId = Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelId")).annotate({ +export const ChannelId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/ChannelId")).annotate({ description: "The ID of the channel where the message is posted", title: "Channel ID", }) export type ChannelId = Schema.Schema.Type -export const ConnectConversationId = Schema.UUID.pipe( +export const ConnectConversationId = Schema.String.check(Schema.isUUID()).pipe( Schema.brand("@HazelChat/ConnectConversationId"), ).annotate({ description: "The ID of a Hazel Connect conversation", @@ -14,7 +14,7 @@ export const ConnectConversationId = Schema.UUID.pipe( }) export type ConnectConversationId = Schema.Schema.Type -export const ConnectConversationChannelId = Schema.UUID.pipe( +export const ConnectConversationChannelId = Schema.String.check(Schema.isUUID()).pipe( Schema.brand("@HazelChat/ConnectConversationChannelId"), ).annotate({ description: "The ID of a Hazel Connect conversation channel mount", @@ -22,13 +22,13 @@ export const ConnectConversationChannelId = Schema.UUID.pipe( }) export type ConnectConversationChannelId = Schema.Schema.Type -export const ConnectInviteId = Schema.UUID.pipe(Schema.brand("@HazelChat/ConnectInviteId")).annotate({ +export const ConnectInviteId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/ConnectInviteId")).annotate({ description: "The ID of a Hazel Connect invite", title: "Connect Invite ID", }) export type ConnectInviteId = Schema.Schema.Type -export const ConnectParticipantId = Schema.UUID.pipe( +export const ConnectParticipantId = Schema.String.check(Schema.isUUID()).pipe( Schema.brand("@HazelChat/ConnectParticipantId"), ).annotate({ description: "The ID of a Hazel Connect participant projection", @@ -36,31 +36,31 @@ export const ConnectParticipantId = Schema.UUID.pipe( }) export type ConnectParticipantId = Schema.Schema.Type -export const UserId = Schema.UUID.pipe(Schema.brand("@HazelChat/UserId")).annotate({ +export const UserId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/UserId")).annotate({ description: "The ID of a user", title: "UserId ID", }) export type UserId = Schema.Schema.Type -export const BotId = Schema.UUID.pipe(Schema.brand("@HazelChat/BotId")).annotate({ +export const BotId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/BotId")).annotate({ description: "The ID of a bot", title: "Bot ID", }) export type BotId = Schema.Schema.Type -export const MessageId = Schema.UUID.pipe(Schema.brand("@HazelChat/MessageId")).annotate({ +export const MessageId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/MessageId")).annotate({ description: "The ID of the message being replied to", title: "Reply To Message ID", }) export type MessageId = Schema.Schema.Type -export const MessageReactionId = Schema.UUID.pipe(Schema.brand("@HazelChat/MessageReactionId")).annotate({ +export const MessageReactionId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/MessageReactionId")).annotate({ description: "The ID of the message reaction", title: "Message Reaction ID", }) export type MessageReactionId = Schema.Schema.Type -export const MessageAttachmentId = Schema.UUID.pipe( +export const MessageAttachmentId = Schema.String.check(Schema.isUUID()).pipe( Schema.brand("@HazelChat/MessageAttachmentId"), ).annotate({ description: "The ID of the message attachment", @@ -68,43 +68,43 @@ export const MessageAttachmentId = Schema.UUID.pipe( }) export type MessageAttachmentId = Schema.Schema.Type -export const AttachmentId = Schema.UUID.pipe(Schema.brand("@HazelChat/AttachmentId")).annotate({ +export const AttachmentId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/AttachmentId")).annotate({ description: "The ID of the attachment being replied to", title: "Attachment ID", }) export type AttachmentId = Schema.Schema.Type -export const OrganizationId = Schema.UUID.pipe(Schema.brand("@HazelChat/OrganizationId")).annotate({ +export const OrganizationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/OrganizationId")).annotate({ description: "The ID of the organization", title: "Organization ID", }) export type OrganizationId = Schema.Schema.Type -export const InvitationId = Schema.UUID.pipe(Schema.brand("@HazelChat/InvitationId")).annotate({ +export const InvitationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/InvitationId")).annotate({ description: "The ID of the invitation", title: "Invitation ID", }) export type InvitationId = Schema.Schema.Type -export const PinnedMessageId = Schema.UUID.pipe(Schema.brand("@HazelChat/PinnedMessageId")).annotate({ +export const PinnedMessageId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/PinnedMessageId")).annotate({ description: "The ID of the pinned message", title: "Pinned Message ID", }) export type PinnedMessageId = Schema.Schema.Type -export const NotificationId = Schema.UUID.pipe(Schema.brand("@HazelChat/NotificationId")).annotate({ +export const NotificationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/NotificationId")).annotate({ description: "The ID of the notification", title: "Notification ID", }) export type NotificationId = Schema.Schema.Type -export const ChannelMemberId = Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelMemberId")).annotate({ +export const ChannelMemberId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/ChannelMemberId")).annotate({ description: "The ID of the channel member", title: "Channel Member ID", }) export type ChannelMemberId = Schema.Schema.Type -export const OrganizationMemberId = Schema.UUID.pipe( +export const OrganizationMemberId = Schema.String.check(Schema.isUUID()).pipe( Schema.brand("@HazelChat/OrganizationMemberId"), ).annotate({ description: "The ID of the organization member", @@ -112,13 +112,13 @@ export const OrganizationMemberId = Schema.UUID.pipe( }) export type OrganizationMemberId = Schema.Schema.Type -export const TypingIndicatorId = Schema.UUID.pipe(Schema.brand("@HazelChat/TypingIndicatorId")).annotate({ +export const TypingIndicatorId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/TypingIndicatorId")).annotate({ description: "The ID of the typing indicator", title: "Typing Indicator ID", }) export type TypingIndicatorId = Schema.Schema.Type -export const UserPresenceStatusId = Schema.UUID.pipe( +export const UserPresenceStatusId = Schema.String.check(Schema.isUUID()).pipe( Schema.brand("@HazelChat/UserPresenceStatusId"), ).annotate({ description: "The ID of the user presence status", @@ -126,7 +126,7 @@ export const UserPresenceStatusId = Schema.UUID.pipe( }) export type UserPresenceStatusId = Schema.Schema.Type -export const IntegrationConnectionId = Schema.UUID.pipe( +export const IntegrationConnectionId = Schema.String.check(Schema.isUUID()).pipe( Schema.brand("@HazelChat/IntegrationConnectionId"), ).annotate({ description: "The ID of an integration connection", @@ -134,13 +134,13 @@ export const IntegrationConnectionId = Schema.UUID.pipe( }) export type IntegrationConnectionId = Schema.Schema.Type -export const SyncConnectionId = Schema.UUID.pipe(Schema.brand("@HazelChat/SyncConnectionId")).annotate({ +export const SyncConnectionId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/SyncConnectionId")).annotate({ description: "The ID of a chat sync connection", title: "Sync Connection ID", }) export type SyncConnectionId = Schema.Schema.Type -export const ExternalChannelId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalChannelId")).annotations( +export const ExternalChannelId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalChannelId")).annotate( { description: "The external channel identifier from a synced provider", title: "External Channel ID", @@ -148,7 +148,7 @@ export const ExternalChannelId = Schema.String.pipe(Schema.brand("@HazelChat/Ext ) export type ExternalChannelId = Schema.Schema.Type -export const ExternalMessageId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalMessageId")).annotations( +export const ExternalMessageId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalMessageId")).annotate( { description: "The external message identifier from a synced provider", title: "External Message ID", @@ -156,7 +156,7 @@ export const ExternalMessageId = Schema.String.pipe(Schema.brand("@HazelChat/Ext ) export type ExternalMessageId = Schema.Schema.Type -export const ExternalWebhookId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalWebhookId")).annotations( +export const ExternalWebhookId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalWebhookId")).annotate( { description: "The external webhook identifier from a synced provider", title: "External Webhook ID", @@ -176,19 +176,19 @@ export const ExternalThreadId = Schema.String.pipe(Schema.brand("@HazelChat/Exte }) export type ExternalThreadId = Schema.Schema.Type -export const SyncChannelLinkId = Schema.UUID.pipe(Schema.brand("@HazelChat/SyncChannelLinkId")).annotate({ +export const SyncChannelLinkId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/SyncChannelLinkId")).annotate({ description: "The ID of a chat sync channel link", title: "Sync Channel Link ID", }) export type SyncChannelLinkId = Schema.Schema.Type -export const SyncMessageLinkId = Schema.UUID.pipe(Schema.brand("@HazelChat/SyncMessageLinkId")).annotate({ +export const SyncMessageLinkId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/SyncMessageLinkId")).annotate({ description: "The ID of a chat sync message link", title: "Sync Message Link ID", }) export type SyncMessageLinkId = Schema.Schema.Type -export const SyncEventReceiptId = Schema.UUID.pipe(Schema.brand("@HazelChat/SyncEventReceiptId")).annotations( +export const SyncEventReceiptId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/SyncEventReceiptId")).annotate( { description: "The ID of a chat sync event receipt", title: "Sync Event Receipt ID", @@ -196,7 +196,7 @@ export const SyncEventReceiptId = Schema.UUID.pipe(Schema.brand("@HazelChat/Sync ) export type SyncEventReceiptId = Schema.Schema.Type -export const IntegrationTokenId = Schema.UUID.pipe(Schema.brand("@HazelChat/IntegrationTokenId")).annotations( +export const IntegrationTokenId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/IntegrationTokenId")).annotate( { description: "The ID of an integration token record", title: "Integration Token ID", @@ -204,7 +204,7 @@ export const IntegrationTokenId = Schema.UUID.pipe(Schema.brand("@HazelChat/Inte ) export type IntegrationTokenId = Schema.Schema.Type -export const MessageIntegrationLinkId = Schema.UUID.pipe( +export const MessageIntegrationLinkId = Schema.String.check(Schema.isUUID()).pipe( Schema.brand("@HazelChat/MessageIntegrationLinkId"), ).annotate({ description: "The ID of a message-integration link", @@ -212,7 +212,7 @@ export const MessageIntegrationLinkId = Schema.UUID.pipe( }) export type MessageIntegrationLinkId = Schema.Schema.Type -export const ChannelWebhookId = Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelWebhookId")).annotate({ +export const ChannelWebhookId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/ChannelWebhookId")).annotate({ description: "The ID of a channel webhook", title: "Channel Webhook ID", }) @@ -224,7 +224,7 @@ export const ChannelIcon = Schema.String.pipe(Schema.brand("@HazelChat/ChannelIc }) export type ChannelIcon = Schema.Schema.Type -export const GitHubSubscriptionId = Schema.UUID.pipe( +export const GitHubSubscriptionId = Schema.String.check(Schema.isUUID()).pipe( Schema.brand("@HazelChat/GitHubSubscriptionId"), ).annotate({ description: "The ID of a GitHub subscription", @@ -232,31 +232,31 @@ export const GitHubSubscriptionId = Schema.UUID.pipe( }) export type GitHubSubscriptionId = Schema.Schema.Type -export const BotCommandId = Schema.UUID.pipe(Schema.brand("@HazelChat/BotCommandId")).annotate({ +export const BotCommandId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/BotCommandId")).annotate({ description: "The ID of a bot command", title: "Bot Command ID", }) export type BotCommandId = Schema.Schema.Type -export const BotInstallationId = Schema.UUID.pipe(Schema.brand("@HazelChat/BotInstallationId")).annotate({ +export const BotInstallationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/BotInstallationId")).annotate({ description: "The ID of a bot installation", title: "Bot Installation ID", }) export type BotInstallationId = Schema.Schema.Type -export const ChannelSectionId = Schema.UUID.pipe(Schema.brand("@HazelChat/ChannelSectionId")).annotate({ +export const ChannelSectionId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/ChannelSectionId")).annotate({ description: "The ID of a channel section", title: "Channel Section ID", }) export type ChannelSectionId = Schema.Schema.Type -export const RssSubscriptionId = Schema.UUID.pipe(Schema.brand("@HazelChat/RssSubscriptionId")).annotate({ +export const RssSubscriptionId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/RssSubscriptionId")).annotate({ description: "The ID of an RSS subscription", title: "RSS Subscription ID", }) export type RssSubscriptionId = Schema.Schema.Type -export const IntegrationRequestId = Schema.UUID.pipe( +export const IntegrationRequestId = Schema.String.check(Schema.isUUID()).pipe( Schema.brand("@HazelChat/IntegrationRequestId"), ).annotate({ description: "The ID of an integration request", @@ -264,13 +264,13 @@ export const IntegrationRequestId = Schema.UUID.pipe( }) export type IntegrationRequestId = Schema.Schema.Type -export const CustomEmojiId = Schema.UUID.pipe(Schema.brand("@HazelChat/CustomEmojiId")).annotate({ +export const CustomEmojiId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/CustomEmojiId")).annotate({ description: "The ID of a custom emoji", title: "Custom Emoji ID", }) export type CustomEmojiId = Schema.Schema.Type -export const MessageOutboxEventId = Schema.UUID.pipe( +export const MessageOutboxEventId = Schema.String.check(Schema.isUUID()).pipe( Schema.brand("@HazelChat/MessageOutboxEventId"), ).annotate({ description: "The ID of a message outbox event", From 3b080c6af987c7e6a4509a041a0be0fd93c411a4 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 08:43:16 +0100 Subject: [PATCH 09/34] xd --- .../policies/typing-indicator-policy.test.ts | 2 +- apps/backend/src/routes/integrations.http.ts | 2 +- .../src/rpc/handlers/channel-webhooks.ts | 2 +- .../chat-sync/chat-sync-core-worker.ts | 4 +- .../chat-sync/chat-sync-provider-registry.ts | 4 +- .../connect-conversation-service.test.ts | 2 +- .../services/message-side-effect-service.ts | 3 +- .../src/services/oauth/oauth-http-client.ts | 5 +- apps/cluster/package.json | 2 +- .../src/services/openrouter-service.ts | 6 +- .../workflows/message-notification-handler.ts | 2 +- apps/electric-proxy/src/auth/bot-auth.ts | 2 +- apps/web/src/db/actions.ts | 2 +- apps/web/src/lib/error-messages.ts | 1 - .../platform-storage/tauri-key-value-store.ts | 2 +- .../src/lib/services/desktop/token-storage.ts | 6 +- .../web/src/lib/services/web/token-storage.ts | 4 +- bots/hazel-bot/package.json | 2 +- bots/hazel-bot/src/openrouter.ts | 2 +- bots/linear-bot/package.json | 2 +- bots/linear-bot/src/index.ts | 2 +- bun.lock | 20 +- libs/ai-openrouter/src/Generated.ts | 2570 ++++++++--------- libs/bot-sdk/src/hazel-bot-sdk.ts | 2 +- libs/bot-sdk/src/retry.ts | 8 +- packages/actors/src/actors/message-actor.ts | 17 +- packages/actors/src/auth/config-service.ts | 29 +- packages/actors/src/auth/jwks-service.test.ts | 10 +- .../src/auth/token-validation-service.ts | 39 +- packages/auth/src/consumers/backend-auth.ts | 7 +- packages/auth/src/consumers/proxy-auth.ts | 9 +- packages/auth/src/session/jwt-decoder.ts | 3 +- .../src/repositories/attachment-repo.ts | 2 +- .../src/repositories/bot-command-repo.ts | 4 +- .../src/repositories/bot-installation-repo.ts | 4 +- .../backend-core/src/repositories/bot-repo.ts | 6 +- .../src/repositories/channel-member-repo.ts | 4 +- .../src/repositories/channel-repo.ts | 2 +- .../src/repositories/channel-section-repo.ts | 2 +- .../src/repositories/channel-webhook-repo.ts | 2 +- .../chat-sync-channel-link-repo.ts | 6 +- .../repositories/chat-sync-connection-repo.ts | 4 +- .../chat-sync-event-receipt-repo.ts | 2 +- .../chat-sync-message-link-repo.ts | 6 +- .../connect-conversation-channel-repo.ts | 4 +- .../repositories/connect-conversation-repo.ts | 2 +- .../src/repositories/connect-invite-repo.ts | 2 +- .../repositories/connect-participant-repo.ts | 2 +- .../src/repositories/custom-emoji-repo.ts | 8 +- .../repositories/github-subscription-repo.ts | 2 +- .../integration-connection-repo.ts | 10 +- .../repositories/integration-token-repo.ts | 2 +- .../src/repositories/invitation-repo.ts | 2 +- .../src/repositories/message-outbox-repo.ts | 6 +- .../src/repositories/message-reaction-repo.ts | 2 +- .../src/repositories/message-repo.ts | 2 +- .../src/repositories/notification-repo.ts | 2 +- .../repositories/organization-member-repo.ts | 4 +- .../src/repositories/organization-repo.ts | 4 +- .../src/repositories/pinned-message-repo.ts | 2 +- .../src/repositories/rss-subscription-repo.ts | 2 +- .../repositories/user-presence-status-repo.ts | 4 +- .../src/repositories/user-repo.ts | 2 +- .../backend-core/src/services/workos-sync.ts | 17 +- packages/db/src/services/database.ts | 25 +- packages/db/src/services/drizzle-effect.ts | 8 +- packages/db/src/services/model-repository.ts | 3 +- packages/db/src/services/model.ts | 1 - packages/domain/src/http/api.ts | 2 +- packages/domain/src/http/chat-sync.ts | 12 +- packages/domain/src/http/incoming-webhooks.ts | 6 +- .../domain/src/http/integration-resources.ts | 10 +- packages/domain/src/http/integrations.ts | 10 +- packages/domain/src/http/presence.ts | 2 +- packages/domain/src/http/webhooks.ts | 4 +- packages/domain/src/models/theme-model.ts | 22 +- packages/domain/src/models/utils.ts | 12 +- .../src/rpc/scope-injection-middleware.ts | 1 - .../domain/src/scopes/current-bot-scopes.ts | 1 - .../domain/src/scopes/current-rpc-scopes.ts | 1 - packages/domain/src/scopes/scope-map.ts | 2 +- packages/domain/src/scopes/validate-scopes.ts | 2 +- packages/integrations/src/craft/api-client.ts | 2 +- .../integrations/src/github/api-client.ts | 2 +- packages/integrations/src/github/payloads.ts | 4 +- .../integrations/src/linear/api-client.ts | 2 +- packages/schema/src/avatar-url.ts | 91 +- packages/schema/src/workos.ts | 10 +- packages/setup/src/commands/bots.ts | 10 +- packages/setup/src/commands/certs.ts | 107 +- packages/setup/src/commands/env.ts | 34 +- packages/setup/src/commands/setup.ts | 44 +- packages/setup/src/index.ts | 25 +- packages/setup/src/services/cert-manager.ts | 2 +- packages/setup/src/services/doctor.ts | 2 +- packages/setup/src/services/env-writer.ts | 2 +- packages/setup/src/services/secrets.ts | 2 +- packages/setup/src/services/validators.ts | 2 +- 98 files changed, 1527 insertions(+), 1836 deletions(-) diff --git a/apps/backend/src/policies/typing-indicator-policy.test.ts b/apps/backend/src/policies/typing-indicator-policy.test.ts index c71534230..7c7532966 100644 --- a/apps/backend/src/policies/typing-indicator-policy.test.ts +++ b/apps/backend/src/policies/typing-indicator-policy.test.ts @@ -26,7 +26,7 @@ const makeChannelMemberRepoLayer = ( ) => Layer.succeed(ChannelMemberRepo, { findByChannelAndUser: (channelId: ChannelId, userId: UserId) => - Effect.succeed(Option.fromNullable(recordsByChannelAndUser[`${channelId}:${userId}`])), + Effect.succeed(Option.fromNullishOr(recordsByChannelAndUser[`${channelId}:${userId}`])), with: (id: ChannelMemberId, f: (member: MemberRecord) => Effect.Effect) => { const member = recordsByMemberId[id] if (!member) { diff --git a/apps/backend/src/routes/integrations.http.ts b/apps/backend/src/routes/integrations.http.ts index 578f472e5..b12e3766a 100644 --- a/apps/backend/src/routes/integrations.http.ts +++ b/apps/backend/src/routes/integrations.http.ts @@ -46,7 +46,7 @@ const OAuthState = Schema.Struct({ * Retry schedule for OAuth operations. * Retries up to 3 times with exponential backoff (100ms, 200ms, 400ms) */ -const oauthRetrySchedule = Schedule.exponential("100 millis").pipe(Schedule.intersect(Schedule.recurs(3))) +const oauthRetrySchedule = Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3))) const CRAFT_ALLOWED_HOST = "connect.craft.do" const CRAFT_BASE_URL_PATH_PATTERN = /^\/links\/[^/]+\/api\/v1$/ diff --git a/apps/backend/src/rpc/handlers/channel-webhooks.ts b/apps/backend/src/rpc/handlers/channel-webhooks.ts index 0f8f1a4ab..2d2db37fb 100644 --- a/apps/backend/src/rpc/handlers/channel-webhooks.ts +++ b/apps/backend/src/rpc/handlers/channel-webhooks.ts @@ -67,7 +67,7 @@ export const ChannelWebhookRpcLive = ChannelWebhookRpcs.toLayer( const { token, tokenHash, tokenSuffix } = generateToken() // Get or create bot user based on whether this is an integration webhook - const botUser = yield* Option.fromNullable(payload.integrationProvider).pipe( + const botUser = yield* Option.fromNullishOr(payload.integrationProvider).pipe( Option.match({ onNone: () => { const botReferenceId = crypto.randomUUID() as ChannelWebhookId diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index 8701892e8..2da64ba32 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -893,7 +893,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() ) .limit(1), ) - return Option.fromNullable(links[0]?.externalMessageId as ExternalMessageId | undefined) + return Option.fromNullishOr(links[0]?.externalMessageId as ExternalMessageId | undefined) }, ) @@ -945,7 +945,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() ) .limit(1), ) - return Option.fromNullable(links[0]?.hazelMessageId) + return Option.fromNullishOr(links[0]?.hazelMessageId) }, ) diff --git a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts index e745af592..892d98035 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts @@ -75,7 +75,7 @@ const DISCORD_MAX_MESSAGE_LENGTH = 2000 const DISCORD_SNOWFLAKE_MIN_LENGTH = 17 const DISCORD_SNOWFLAKE_MAX_LENGTH = 30 const DISCORD_THREAD_NAME_MAX_LENGTH = 100 -const DISCORD_SYNC_RETRY_SCHEDULE = Schedule.intersect( +const DISCORD_SYNC_RETRY_SCHEDULE = Schedule.both( Schedule.exponential("250 millis").pipe(Schedule.jittered), Schedule.recurs(3), ) @@ -422,7 +422,7 @@ export class ChatSyncProviderRegistry extends ServiceMap.Service const getAdapter = Effect.fn("ChatSyncProviderRegistry.getAdapter")(function* (provider: string) { - const adapter = Option.fromNullable(adapters[provider as keyof typeof adapters]) + const adapter = Option.fromNullishOr(adapters[provider as keyof typeof adapters]) return yield* Option.match(adapter, { onNone: () => Effect.fail( diff --git a/apps/backend/src/services/connect-conversation-service.test.ts b/apps/backend/src/services/connect-conversation-service.test.ts index 5b519f855..844f0f4bf 100644 --- a/apps/backend/src/services/connect-conversation-service.test.ts +++ b/apps/backend/src/services/connect-conversation-service.test.ts @@ -105,7 +105,7 @@ const makeMountRepoLayer = (mounts: MutableMount[]) => Layer.succeed(ConnectConversationChannelRepo, { findByChannelId: (channelId: ChannelId) => Effect.succeed( - Option.fromNullable( + Option.fromNullishOr( mounts.find((mount) => mount.channelId === channelId && mount.deletedAt === null), ), ), diff --git a/apps/backend/src/services/message-side-effect-service.ts b/apps/backend/src/services/message-side-effect-service.ts index 141b87a76..5d541b1ba 100644 --- a/apps/backend/src/services/message-side-effect-service.ts +++ b/apps/backend/src/services/message-side-effect-service.ts @@ -2,7 +2,6 @@ import { HttpApiClient } from "effect/unstable/httpapi" import { and, Database, eq, isNull, schema, sql } from "@hazel/db" import { Cluster, WorkflowInitializationError } from "@hazel/domain" import { ServiceMap, Array, Config, Effect, Layer, Option } from "effect" -import { TreeFormatter } from "effect/ParseResult" import type { MessageCreatedPayload, MessageDeletedPayload, @@ -120,7 +119,7 @@ export class MessageSideEffectService extends ServiceMap.Service diff --git a/apps/backend/src/services/oauth/oauth-http-client.ts b/apps/backend/src/services/oauth/oauth-http-client.ts index 1faec0c86..05cc27f54 100644 --- a/apps/backend/src/services/oauth/oauth-http-client.ts +++ b/apps/backend/src/services/oauth/oauth-http-client.ts @@ -7,7 +7,6 @@ import { FetchHttpClient, HttpBody, HttpClient } from "effect/unstable/http" import { ServiceMap, Duration, Effect, Layer, Schema } from "effect" -import { TreeFormatter } from "effect/ParseResult" import type { OAuthIntegrationProvider } from "./provider-config" // ============================================================================ @@ -130,7 +129,7 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut Effect.catchTags({ ParseError: (error) => new OAuthHttpError({ - message: `Failed to parse token response: ${TreeFormatter.formatErrorSync(error)}`, + message: `Failed to parse token response: ${String(error)}`, cause: error, }), ResponseError: (error) => @@ -195,7 +194,7 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut Effect.catchTags({ ParseError: (error) => new OAuthHttpError({ - message: `Failed to parse token response: ${TreeFormatter.formatErrorSync(error)}`, + message: `Failed to parse token response: ${String(error)}`, cause: error, }), ResponseError: (error) => diff --git a/apps/cluster/package.json b/apps/cluster/package.json index 0c882235f..21425fdbc 100644 --- a/apps/cluster/package.json +++ b/apps/cluster/package.json @@ -11,7 +11,7 @@ "dependencies": { "@effect/platform-bun": "catalog:effect", "@effect/sql-pg": "catalog:effect", - "@hazel/ai-openrouter": "workspace:*", + "@effect/ai-openrouter": "catalog:effect", "@hazel/backend-core": "workspace:*", "@hazel/db": "workspace:*", "@hazel/domain": "workspace:*", diff --git a/apps/cluster/src/services/openrouter-service.ts b/apps/cluster/src/services/openrouter-service.ts index 78ef1bfc5..7bc29a295 100644 --- a/apps/cluster/src/services/openrouter-service.ts +++ b/apps/cluster/src/services/openrouter-service.ts @@ -1,12 +1,12 @@ -import { OpenRouterClient, OpenRouterLanguageModel } from "@hazel/ai-openrouter" +import { OpenRouterClient, OpenRouterLanguageModel } from "@effect/ai-openrouter" import { FetchHttpClient } from "effect/unstable/http" import { Config, Layer } from "effect" // OpenRouter configuration from environment const OpenRouterClientLayer = OpenRouterClient.layerConfig({ apiKey: Config.redacted("OPENROUTER_API_KEY"), - referrer: Config.string("APP_URL").pipe(Config.withDefault("https://app.hazel.sh")), - title: Config.string("APP_NAME").pipe(Config.withDefault("Hazel")), + siteReferrer: Config.string("APP_URL").pipe(Config.withDefault("https://app.hazel.sh")), + siteTitle: Config.string("APP_NAME").pipe(Config.withDefault("Hazel")), }).pipe(Layer.provide(FetchHttpClient.layer)) const MODEL = "anthropic/claude-3.5-haiku" diff --git a/apps/cluster/src/workflows/message-notification-handler.ts b/apps/cluster/src/workflows/message-notification-handler.ts index 2a876c79a..ba04c466d 100644 --- a/apps/cluster/src/workflows/message-notification-handler.ts +++ b/apps/cluster/src/workflows/message-notification-handler.ts @@ -381,7 +381,7 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf ) const insertedChannelMemberIds = Array.filterMap(insertedNotifications, (row) => - Option.fromNullable(channelMemberByOrgMember.get(row.memberId)), + Option.fromNullishOr(channelMemberByOrgMember.get(row.memberId)), ) if (insertedChannelMemberIds.length > 0) { diff --git a/apps/electric-proxy/src/auth/bot-auth.ts b/apps/electric-proxy/src/auth/bot-auth.ts index 66130e3bc..8e65916f3 100644 --- a/apps/electric-proxy/src/auth/bot-auth.ts +++ b/apps/electric-proxy/src/auth/bot-auth.ts @@ -78,7 +78,7 @@ export const validateBotToken = Effect.fn("ElectricProxy.validateBotToken")(func .where(and(eq(schema.botsTable.apiTokenHash, tokenHash), isNull(schema.botsTable.deletedAt))) .limit(1), ) - const botOption = Option.fromNullable(botResult[0]) + const botOption = Option.fromNullishOr(botResult[0]) if (Option.isNone(botOption)) { yield* Effect.annotateCurrentSpan("auth.token.valid", false) diff --git a/apps/web/src/db/actions.ts b/apps/web/src/db/actions.ts index 06c74a99d..dbc519f59 100644 --- a/apps/web/src/db/actions.ts +++ b/apps/web/src/db/actions.ts @@ -55,7 +55,7 @@ const getMountedConversationId = (channelId: ChannelId) => const MessageRetrySchedule = Schedule.exponential(Duration.seconds(1), 2).pipe( Schedule.jittered, Schedule.whileInput(isErrorRetryable), - Schedule.intersect(Schedule.recurs(3)), + Schedule.both(Schedule.recurs(3)), ) export const sendMessageAction = optimisticAction({ diff --git a/apps/web/src/lib/error-messages.ts b/apps/web/src/lib/error-messages.ts index 1f445f813..e242f5f1b 100644 --- a/apps/web/src/lib/error-messages.ts +++ b/apps/web/src/lib/error-messages.ts @@ -33,7 +33,6 @@ import { WorkOSUserFetchError, } from "@hazel/domain/errors" import { Cause, Chunk, Match, Option, Schema } from "effect" -import type { ParseError } from "effect/ParseResult" import { CollectionInErrorEffectError, CollectionSyncEffectError, diff --git a/apps/web/src/lib/platform-storage/tauri-key-value-store.ts b/apps/web/src/lib/platform-storage/tauri-key-value-store.ts index 3ff031503..195386ec3 100644 --- a/apps/web/src/lib/platform-storage/tauri-key-value-store.ts +++ b/apps/web/src/lib/platform-storage/tauri-key-value-store.ts @@ -59,7 +59,7 @@ export const layerTauriStore: Layer.Layer = Layer.e Effect.tryPromise({ try: async () => { const value = await store.get(key) - return Option.fromNullable(value) + return Option.fromNullishOr(value) }, catch: (error) => makeError("get", key, error), }), diff --git a/apps/web/src/lib/services/desktop/token-storage.ts b/apps/web/src/lib/services/desktop/token-storage.ts index 94ce8b001..2a607963c 100644 --- a/apps/web/src/lib/services/desktop/token-storage.ts +++ b/apps/web/src/lib/services/desktop/token-storage.ts @@ -103,7 +103,7 @@ export class TokenStorage extends ServiceMap.Service()("TokenStora detail: String(e), }), }) - return Option.fromNullable(token) + return Option.fromNullishOr(token) }), /** @@ -120,7 +120,7 @@ export class TokenStorage extends ServiceMap.Service()("TokenStora detail: String(e), }), }) - return Option.fromNullable(token) + return Option.fromNullishOr(token) }), /** @@ -137,7 +137,7 @@ export class TokenStorage extends ServiceMap.Service()("TokenStora detail: String(e), }), }) - return Option.fromNullable(expiresAt) + return Option.fromNullishOr(expiresAt) }), /** diff --git a/apps/web/src/lib/services/web/token-storage.ts b/apps/web/src/lib/services/web/token-storage.ts index 786109cfe..f974e6775 100644 --- a/apps/web/src/lib/services/web/token-storage.ts +++ b/apps/web/src/lib/services/web/token-storage.ts @@ -83,7 +83,7 @@ export class WebTokenStorage extends ServiceMap.Service()("WebT detail: String(e), }), }) - return Option.fromNullable(token) + return Option.fromNullishOr(token) }), /** @@ -100,7 +100,7 @@ export class WebTokenStorage extends ServiceMap.Service()("WebT detail: String(e), }), }) - return Option.fromNullable(token) + return Option.fromNullishOr(token) }), /** diff --git a/bots/hazel-bot/package.json b/bots/hazel-bot/package.json index 9b6015232..22ebe16b5 100644 --- a/bots/hazel-bot/package.json +++ b/bots/hazel-bot/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@hazel-chat/bot-sdk": "workspace:*", - "@hazel/ai-openrouter": "workspace:*", + "@effect/ai-openrouter": "catalog:effect", "@hazel/domain": "workspace:*", "@hazel/integrations": "workspace:*", "@hazel/schema": "workspace:*", diff --git a/bots/hazel-bot/src/openrouter.ts b/bots/hazel-bot/src/openrouter.ts index f41e67093..2403a7a9f 100644 --- a/bots/hazel-bot/src/openrouter.ts +++ b/bots/hazel-bot/src/openrouter.ts @@ -1,4 +1,4 @@ -import { OpenRouterClient, OpenRouterLanguageModel } from "@hazel/ai-openrouter" +import { OpenRouterClient, OpenRouterLanguageModel } from "@effect/ai-openrouter" import { FetchHttpClient } from "effect/unstable/http" import { Config, Effect, Layer } from "effect" diff --git a/bots/linear-bot/package.json b/bots/linear-bot/package.json index a0542010c..81f15f5c8 100644 --- a/bots/linear-bot/package.json +++ b/bots/linear-bot/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@hazel-chat/bot-sdk": "workspace:*", - "@hazel/ai-openrouter": "workspace:*", + "@effect/ai-openrouter": "catalog:effect", "@hazel/db": "workspace:*", "@hazel/domain": "workspace:*", "@hazel/integrations": "workspace:*", diff --git a/bots/linear-bot/src/index.ts b/bots/linear-bot/src/index.ts index b7d84076a..761c1dcd7 100644 --- a/bots/linear-bot/src/index.ts +++ b/bots/linear-bot/src/index.ts @@ -1,5 +1,5 @@ import { LanguageModel } from "effect/unstable/ai" -import { OpenRouterClient, OpenRouterLanguageModel } from "@hazel/ai-openrouter" +import { OpenRouterClient, OpenRouterLanguageModel } from "@effect/ai-openrouter" import { FetchHttpClient } from "effect/unstable/http" import { Config, Effect, Layer, Schema } from "effect" import { runHazelBot } from "@hazel-chat/bot-sdk" diff --git a/bun.lock b/bun.lock index 394b17308..bad4e2943 100644 --- a/bun.lock +++ b/bun.lock @@ -79,9 +79,9 @@ "apps/cluster": { "name": "cluster", "dependencies": { + "@effect/ai-openrouter": "catalog:effect", "@effect/platform-bun": "catalog:effect", "@effect/sql-pg": "catalog:effect", - "@hazel/ai-openrouter": "workspace:*", "@hazel/backend-core": "workspace:*", "@hazel/db": "workspace:*", "@hazel/domain": "workspace:*", @@ -335,8 +335,8 @@ "name": "hazel-bot", "version": "1.0.0", "dependencies": { + "@effect/ai-openrouter": "catalog:effect", "@hazel-chat/bot-sdk": "workspace:*", - "@hazel/ai-openrouter": "workspace:*", "@hazel/domain": "workspace:*", "@hazel/integrations": "workspace:*", "@hazel/schema": "workspace:*", @@ -350,8 +350,8 @@ "name": "linear-bot", "version": "1.0.0", "dependencies": { + "@effect/ai-openrouter": "catalog:effect", "@hazel-chat/bot-sdk": "workspace:*", - "@hazel/ai-openrouter": "workspace:*", "@hazel/db": "workspace:*", "@hazel/domain": "workspace:*", "@hazel/integrations": "workspace:*", @@ -1240,7 +1240,7 @@ "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], - "@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + "@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg=="], @@ -4476,14 +4476,24 @@ "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/otlp-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], + "@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], diff --git a/libs/ai-openrouter/src/Generated.ts b/libs/ai-openrouter/src/Generated.ts index 052825191..9a2452c50 100644 --- a/libs/ai-openrouter/src/Generated.ts +++ b/libs/ai-openrouter/src/Generated.ts @@ -1,102 +1,94 @@ /** * @since 1.0.0 */ -import type * as HttpClient from "@effect/platform/HttpClient" -import * as HttpClientError from "@effect/platform/HttpClientError" -import * as HttpClientRequest from "@effect/platform/HttpClientRequest" -import * as HttpClientResponse from "@effect/platform/HttpClientResponse" +import type * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" import * as Data from "effect/Data" import * as Effect from "effect/Effect" -import type { ParseError } from "effect/ParseResult" import * as S from "effect/Schema" export class CacheControlEphemeral extends S.Class("CacheControlEphemeral")({ type: S.Literal("ephemeral"), }) {} -export class OpenResponsesReasoningFormat extends S.Literal( - "unknown", +export const OpenResponsesReasoningFormat = S.Literals(["unknown", "openai-responses-v1", "azure-openai-responses-v1", "xai-responses-v1", "anthropic-claude-v1", - "google-gemini-v1", -) {} + "google-gemini-v1",]) -export class OpenResponsesReasoningType extends S.Literal("reasoning") {} +export const OpenResponsesReasoningType = S.Literal("reasoning") -export class ReasoningTextContentType extends S.Literal("reasoning_text") {} +export const ReasoningTextContentType = S.Literal("reasoning_text") export class ReasoningTextContent extends S.Class("ReasoningTextContent")({ type: ReasoningTextContentType, text: S.String, }) {} -export class ReasoningSummaryTextType extends S.Literal("summary_text") {} +export const ReasoningSummaryTextType = S.Literal("summary_text") export class ReasoningSummaryText extends S.Class("ReasoningSummaryText")({ type: ReasoningSummaryTextType, text: S.String, }) {} -export class OpenResponsesReasoningStatusEnum extends S.Literal("in_progress") {} +export const OpenResponsesReasoningStatusEnum = S.Literal("in_progress") export class OpenResponsesReasoning extends S.Class("OpenResponsesReasoning")({ - signature: S.optionalWith(S.String, { nullable: true }), - format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), + signature: S.optional(S.NullOr(S.String)), + format: S.optional(S.NullOr(OpenResponsesReasoningFormat)), type: OpenResponsesReasoningType, id: S.String, - content: S.optionalWith(S.Array(ReasoningTextContent), { nullable: true }), + content: S.optional(S.NullOr(S.Array(ReasoningTextContent))), summary: S.Array(ReasoningSummaryText), - encrypted_content: S.optionalWith(S.String, { nullable: true }), - status: S.optionalWith( - S.Union( + encrypted_content: S.optional(S.NullOr(S.String)), + status: S.optional(S.NullOr(S.Union([ OpenResponsesReasoningStatusEnum, OpenResponsesReasoningStatusEnum, OpenResponsesReasoningStatusEnum, - ), - { nullable: true }, - ), + ]))), }) {} export class ReasoningDetailSummary extends S.Class("ReasoningDetailSummary")({ - id: S.optionalWith(S.String, { nullable: true }), + id: S.optional(S.NullOr(S.String)), type: S.Literal("reasoning.summary"), index: S.optional(S.Number), - format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), + format: S.optional(S.NullOr(OpenResponsesReasoningFormat)), summary: S.String, }) {} export class ReasoningDetailEncrypted extends S.Class("ReasoningDetailEncrypted")({ - id: S.optionalWith(S.String, { nullable: true }), + id: S.optional(S.NullOr(S.String)), type: S.Literal("reasoning.encrypted"), index: S.optional(S.Number), - format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), + format: S.optional(S.NullOr(OpenResponsesReasoningFormat)), data: S.String, }) {} export class ReasoningDetailText extends S.Class("ReasoningDetailText")({ - id: S.optionalWith(S.String, { nullable: true }), + id: S.optional(S.NullOr(S.String)), type: S.Literal("reasoning.text"), index: S.optional(S.Number), - format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), - text: S.optionalWith(S.String, { nullable: true }), - signature: S.optionalWith(S.String, { nullable: true }), + format: S.optional(S.NullOr(OpenResponsesReasoningFormat)), + text: S.optional(S.NullOr(S.String)), + signature: S.optional(S.NullOr(S.String)), }) {} -export class ReasoningDetail extends S.Union( - ReasoningDetailSummary, +export const ReasoningDetail = S.Union([ReasoningDetailSummary, ReasoningDetailEncrypted, - ReasoningDetailText, -) {} + ReasoningDetailText,]) export class FileAnnotationDetail extends S.Class("FileAnnotationDetail")({ type: S.Literal("file"), file: S.Struct({ hash: S.String, - name: S.optionalWith(S.String, { nullable: true }), + name: S.optional(S.NullOr(S.String)), content: S.Array( - S.Union( + S.Union([ S.Struct({ type: S.Literal("text"), text: S.String, @@ -107,7 +99,7 @@ export class FileAnnotationDetail extends S.Class("FileAnn url: S.String, }), }), - ), + ]), ), }), }) {} @@ -121,17 +113,17 @@ export class URLCitationAnnotationDetail extends S.Class("ResponseInput text: S.String, }) {} -export class ResponseInputFileType extends S.Literal("input_file") {} +export const ResponseInputFileType = S.Literal("input_file") /** * File input content item */ export class ResponseInputFile extends S.Class("ResponseInputFile")({ type: ResponseInputFileType, - file_id: S.optionalWith(S.String, { nullable: true }), - file_data: S.optionalWith(S.String, { nullable: true }), - filename: S.optionalWith(S.String, { nullable: true }), - file_url: S.optionalWith(S.String, { nullable: true }), + file_id: S.optional(S.NullOr(S.String)), + file_data: S.optional(S.NullOr(S.String)), + filename: S.optional(S.NullOr(S.String)), + file_url: S.optional(S.NullOr(S.String)), }) {} -export class ResponseInputAudioType extends S.Literal("input_audio") {} +export const ResponseInputAudioType = S.Literal("input_audio") -export class ResponseInputAudioInputAudioFormat extends S.Literal("mp3", "wav") {} +export const ResponseInputAudioInputAudioFormat = S.Literals(["mp3", "wav"]) /** * Audio input content item @@ -169,7 +161,7 @@ export class ResponseInputAudio extends S.Class("ResponseInp }), }) {} -export class ResponseInputVideoType extends S.Literal("input_video") {} +export const ResponseInputVideoType = S.Literal("input_video") /** * Video input content item @@ -185,69 +177,69 @@ export class ResponseInputVideo extends S.Class("ResponseInp export class OpenResponsesEasyInputMessage extends S.Class( "OpenResponsesEasyInputMessage", )({ - type: S.optionalWith(OpenResponsesEasyInputMessageType, { nullable: true }), - role: S.Union( + type: S.optional(S.NullOr(OpenResponsesEasyInputMessageType)), + role: S.Union([ OpenResponsesEasyInputMessageRoleEnum, OpenResponsesEasyInputMessageRoleEnum, OpenResponsesEasyInputMessageRoleEnum, OpenResponsesEasyInputMessageRoleEnum, - ), - content: S.Union( + ]), + content: S.Union([ S.Array( - S.Union( + S.Union([ ResponseInputText, /** * Image input content item */ S.Struct({ type: S.Literal("input_image"), - detail: S.Literal("auto", "high", "low"), - image_url: S.optionalWith(S.String, { nullable: true }), + detail: S.Literals(["auto", "high", "low"]), + image_url: S.optional(S.NullOr(S.String)), }), ResponseInputFile, ResponseInputAudio, ResponseInputVideo, - ), + ]), ), S.String, - ), + ]), }) {} -export class OpenResponsesInputMessageItemType extends S.Literal("message") {} +export const OpenResponsesInputMessageItemType = S.Literal("message") -export class OpenResponsesInputMessageItemRoleEnum extends S.Literal("developer") {} +export const OpenResponsesInputMessageItemRoleEnum = S.Literal("developer") export class OpenResponsesInputMessageItem extends S.Class( "OpenResponsesInputMessageItem", )({ - id: S.optionalWith(S.String, { nullable: true }), - type: S.optionalWith(OpenResponsesInputMessageItemType, { nullable: true }), - role: S.Union( + id: S.optional(S.NullOr(S.String)), + type: S.optional(S.NullOr(OpenResponsesInputMessageItemType)), + role: S.Union([ OpenResponsesInputMessageItemRoleEnum, OpenResponsesInputMessageItemRoleEnum, OpenResponsesInputMessageItemRoleEnum, - ), + ]), content: S.Array( - S.Union( + S.Union([ ResponseInputText, /** * Image input content item */ S.Struct({ type: S.Literal("input_image"), - detail: S.Literal("auto", "high", "low"), - image_url: S.optionalWith(S.String, { nullable: true }), + detail: S.Literals(["auto", "high", "low"]), + image_url: S.optional(S.NullOr(S.String)), }), ResponseInputFile, ResponseInputAudio, ResponseInputVideo, - ), + ]), ), }) {} -export class OpenResponsesFunctionToolCallType extends S.Literal("function_call") {} +export const OpenResponsesFunctionToolCallType = S.Literal("function_call") -export class ToolCallStatus extends S.Literal("in_progress", "completed", "incomplete") {} +export const ToolCallStatus = S.Literals(["in_progress", "completed", "incomplete"]) /** * A function call initiated by the model @@ -260,10 +252,10 @@ export class OpenResponsesFunctionToolCall extends S.Class("FileCitation")({ type: FileCitationType, @@ -295,7 +287,7 @@ export class FileCitation extends S.Class("FileCitation")({ index: S.Number, }) {} -export class URLCitationType extends S.Literal("url_citation") {} +export const URLCitationType = S.Literal("url_citation") export class URLCitation extends S.Class("URLCitation")({ type: URLCitationType, @@ -305,7 +297,7 @@ export class URLCitation extends S.Class("URLCitation")({ end_index: S.Number, }) {} -export class FilePathType extends S.Literal("file_path") {} +export const FilePathType = S.Literal("file_path") export class FilePath extends S.Class("FilePath")({ type: FilePathType, @@ -313,14 +305,13 @@ export class FilePath extends S.Class("FilePath")({ index: S.Number, }) {} -export class OpenAIResponsesAnnotation extends S.Union(FileCitation, URLCitation, FilePath) {} +export const OpenAIResponsesAnnotation = S.Union([FileCitation, URLCitation, FilePath]) export class ResponseOutputText extends S.Class("ResponseOutputText")({ type: ResponseOutputTextType, text: S.String, - annotations: S.optionalWith(S.Array(OpenAIResponsesAnnotation), { nullable: true }), - logprobs: S.optionalWith( - S.Array( + annotations: S.optional(S.NullOr(S.Array(OpenAIResponsesAnnotation))), + logprobs: S.optional(S.NullOr(S.Array( S.Struct({ token: S.String, bytes: S.Array(S.Number), @@ -333,12 +324,10 @@ export class ResponseOutputText extends S.Class("ResponseOut }), ), }), - ), - { nullable: true }, - ), + ))), }) {} -export class OpenAIResponsesRefusalContentType extends S.Literal("refusal") {} +export const OpenAIResponsesRefusalContentType = S.Literal("refusal") export class OpenAIResponsesRefusalContent extends S.Class( "OpenAIResponsesRefusalContent", @@ -351,32 +340,27 @@ export class ResponsesOutputMessage extends S.Class("Res id: S.String, role: ResponsesOutputMessageRole, type: ResponsesOutputMessageType, - status: S.optionalWith( - S.Union( + status: S.optional(S.NullOr(S.Union([ ResponsesOutputMessageStatusEnum, ResponsesOutputMessageStatusEnum, ResponsesOutputMessageStatusEnum, - ), - { nullable: true }, - ), - content: S.Array(S.Union(ResponseOutputText, OpenAIResponsesRefusalContent)), + ]))), + content: S.Array(S.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), }) {} /** * The format of the reasoning content */ -export class ResponsesOutputItemReasoningFormat extends S.Literal( - "unknown", +export const ResponsesOutputItemReasoningFormat = S.Literals(["unknown", "openai-responses-v1", "azure-openai-responses-v1", "xai-responses-v1", "anthropic-claude-v1", - "google-gemini-v1", -) {} + "google-gemini-v1",]) -export class ResponsesOutputItemReasoningType extends S.Literal("reasoning") {} +export const ResponsesOutputItemReasoningType = S.Literal("reasoning") -export class ResponsesOutputItemReasoningStatusEnum extends S.Literal("in_progress") {} +export const ResponsesOutputItemReasoningStatusEnum = S.Literal("in_progress") export class ResponsesOutputItemReasoning extends S.Class( "ResponsesOutputItemReasoning", @@ -384,51 +368,45 @@ export class ResponsesOutputItemReasoning extends S.Class( "ResponsesOutputItemFunctionCall", )({ type: ResponsesOutputItemFunctionCallType, - id: S.optionalWith(S.String, { nullable: true }), + id: S.optional(S.NullOr(S.String)), name: S.String, arguments: S.String, call_id: S.String, - status: S.optionalWith( - S.Union( + status: S.optional(S.NullOr(S.Union([ ResponsesOutputItemFunctionCallStatusEnum, ResponsesOutputItemFunctionCallStatusEnum, ResponsesOutputItemFunctionCallStatusEnum, - ), - { nullable: true }, - ), + ]))), }) {} -export class ResponsesWebSearchCallOutputType extends S.Literal("web_search_call") {} +export const ResponsesWebSearchCallOutputType = S.Literal("web_search_call") -export class WebSearchStatus extends S.Literal("completed", "searching", "in_progress", "failed") {} +export const WebSearchStatus = S.Literals(["completed", "searching", "in_progress", "failed"]) export class ResponsesWebSearchCallOutput extends S.Class( "ResponsesWebSearchCallOutput", @@ -438,7 +416,7 @@ export class ResponsesWebSearchCallOutput extends S.Class( "ResponsesOutputItemFileSearchCall", @@ -449,26 +427,25 @@ export class ResponsesOutputItemFileSearchCall extends S.Class( "ResponsesImageGenerationCall", )({ type: ResponsesImageGenerationCallType, id: S.String, - result: S.optionalWith(S.NullOr(S.String), { default: () => null }), + result: S.NullOr(S.String).pipe(S.optional, S.withDecodingDefault(() => null)), status: ImageGenerationStatus, }) {} /** * Input for a response request - can be a string or array of items */ -export class OpenResponsesInput extends S.Union( - S.String, +export const OpenResponsesInput = S.Union([S.String, S.Array( - S.Union( + S.Union([ OpenResponsesReasoning, OpenResponsesEasyInputMessage, OpenResponsesInputMessageItem, @@ -480,32 +457,31 @@ export class OpenResponsesInput extends S.Union( ResponsesWebSearchCallOutput, ResponsesOutputItemFileSearchCall, ResponsesImageGenerationCall, - ), - ), -) {} + ]), + ),]) /** * Metadata key-value pairs for the request. Keys must be ≤64 characters and cannot contain brackets. Values must be ≤512 characters. Maximum 16 pairs allowed. */ -export class OpenResponsesRequestMetadata extends S.Record({ key: S.String, value: S.Unknown }) {} +export const OpenResponsesRequestMetadata = S.Record(S.String, S.Unknown) -export class OpenResponsesWebSearchPreviewToolType extends S.Literal("web_search_preview") {} +export const OpenResponsesWebSearchPreviewToolType = S.Literal("web_search_preview") /** * Size of the search context for web search tools */ -export class ResponsesSearchContextSize extends S.Literal("low", "medium", "high") {} +export const ResponsesSearchContextSize = S.Literals(["low", "medium", "high"]) -export class WebSearchPreviewToolUserLocationType extends S.Literal("approximate") {} +export const WebSearchPreviewToolUserLocationType = S.Literal("approximate") export class WebSearchPreviewToolUserLocation extends S.Class( "WebSearchPreviewToolUserLocation", )({ type: WebSearchPreviewToolUserLocationType, - city: S.optionalWith(S.String, { nullable: true }), - country: S.optionalWith(S.String, { nullable: true }), - region: S.optionalWith(S.String, { nullable: true }), - timezone: S.optionalWith(S.String, { nullable: true }), + city: S.optional(S.NullOr(S.String)), + country: S.optional(S.NullOr(S.String)), + region: S.optional(S.NullOr(S.String)), + timezone: S.optional(S.NullOr(S.String)), }) {} /** @@ -515,13 +491,11 @@ export class OpenResponsesWebSearchPreviewTool extends S.Class( "ResponsesWebSearchUserLocation", )({ - type: S.optionalWith(ResponsesWebSearchUserLocationType, { nullable: true }), - city: S.optionalWith(S.String, { nullable: true }), - country: S.optionalWith(S.String, { nullable: true }), - region: S.optionalWith(S.String, { nullable: true }), - timezone: S.optionalWith(S.String, { nullable: true }), + type: S.optional(S.NullOr(ResponsesWebSearchUserLocationType)), + city: S.optional(S.NullOr(S.String)), + country: S.optional(S.NullOr(S.String)), + region: S.optional(S.NullOr(S.String)), + timezone: S.optional(S.NullOr(S.String)), }) {} /** @@ -558,17 +532,14 @@ export class OpenResponsesWebSearchTool extends S.Class("Responses type: ResponsesFormatTextType, }) {} -export class ResponsesFormatJSONObjectType extends S.Literal("json_object") {} +export const ResponsesFormatJSONObjectType = S.Literal("json_object") /** * JSON object response format @@ -626,7 +592,7 @@ export class ResponsesFormatJSONObject extends S.Class( "OpenResponsesResponseText", )({ - format: S.optionalWith(ResponseFormatTextConfig, { nullable: true }), - verbosity: S.optionalWith(OpenResponsesResponseTextVerbosity, { nullable: true }), + format: S.optional(S.NullOr(ResponseFormatTextConfig)), + verbosity: S.optional(S.NullOr(OpenResponsesResponseTextVerbosity)), }) {} -export class OpenAIResponsesReasoningEffort extends S.Literal( - "xhigh", +export const OpenAIResponsesReasoningEffort = S.Literals(["xhigh", "high", "medium", "low", "minimal", - "none", -) {} + "none",]) -export class ReasoningSummaryVerbosity extends S.Literal("auto", "concise", "detailed") {} +export const ReasoningSummaryVerbosity = S.Literals(["auto", "concise", "detailed"]) export class OpenResponsesReasoningConfig extends S.Class( "OpenResponsesReasoningConfig", )({ - max_tokens: S.optionalWith(S.Number, { nullable: true }), - enabled: S.optionalWith(S.Boolean, { nullable: true }), - effort: S.optionalWith(OpenAIResponsesReasoningEffort, { nullable: true }), - summary: S.optionalWith(ReasoningSummaryVerbosity, { nullable: true }), + max_tokens: S.optional(S.NullOr(S.Number)), + enabled: S.optional(S.NullOr(S.Boolean)), + effort: S.optional(S.NullOr(OpenAIResponsesReasoningEffort)), + summary: S.optional(S.NullOr(ReasoningSummaryVerbosity)), }) {} -export class ResponsesOutputModality extends S.Literal("text", "image") {} +export const ResponsesOutputModality = S.Literals(["text", "image"]) export class OpenAIResponsesPrompt extends S.Class("OpenAIResponsesPrompt")({ id: S.String, - variables: S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + variables: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), }) {} -export class OpenAIResponsesIncludable extends S.Literal( - "file_search_call.results", +export const OpenAIResponsesIncludable = S.Literals(["file_search_call.results", "message.input_image.image_url", "computer_call_output.output.image_url", "reasoning.encrypted_content", - "code_interpreter_call.outputs", -) {} + "code_interpreter_call.outputs",]) -export class OpenResponsesRequestServiceTier extends S.Literal("auto") {} +export const OpenResponsesRequestServiceTier = S.Literal("auto") -export class OpenResponsesRequestTruncationEnum extends S.Literal("auto", "disabled") {} +export const OpenResponsesRequestTruncationEnum = S.Literals(["auto", "disabled"]) -export class OpenResponsesRequestTruncation extends OpenResponsesRequestTruncationEnum {} +export const OpenResponsesRequestTruncation = OpenResponsesRequestTruncationEnum /** * Data collection setting. If no available model provider meets the requirement, your request will return an error. @@ -709,10 +669,9 @@ export class OpenResponsesRequestTruncation extends OpenResponsesRequestTruncati * * - deny: use only providers which do not collect user data. */ -export class DataCollection extends S.Literal("deny", "allow") {} +export const DataCollection = S.Literals(["deny", "allow"]) -export class ProviderName extends S.Literal( - "AI21", +export const ProviderName = S.Literals(["AI21", "AionLabs", "Alibaba", "Amazon Bedrock", @@ -781,11 +740,9 @@ export class ProviderName extends S.Literal( "Xiaomi", "xAI", "Z.AI", - "FakeProvider", -) {} + "FakeProvider",]) -export class Quantization extends S.Literal( - "int4", +export const Quantization = S.Literals(["int4", "int8", "fp4", "fp6", @@ -793,22 +750,21 @@ export class Quantization extends S.Literal( "fp16", "bf16", "fp32", - "unknown", -) {} + "unknown",]) -export class ProviderSort extends S.Literal("price", "throughput", "latency") {} +export const ProviderSort = S.Literals(["price", "throughput", "latency"]) -export class ProviderSortConfigPartitionEnum extends S.Literal("model", "none") {} +export const ProviderSortConfigPartitionEnum = S.Literals(["model", "none"]) export class ProviderSortConfig extends S.Class("ProviderSortConfig")({ - by: S.optionalWith(ProviderSort, { nullable: true }), - partition: S.optionalWith(ProviderSortConfigPartitionEnum, { nullable: true }), + by: S.optional(S.NullOr(ProviderSort)), + partition: S.optional(S.NullOr(ProviderSortConfigPartitionEnum)), }) {} /** * A value in string format that is a large number */ -export class BigNumberUnion extends S.String {} +export const BigNumberUnion = S.String /** * Percentile-based throughput cutoffs. All specified cutoffs must be met for an endpoint to be preferred. @@ -819,25 +775,25 @@ export class PercentileThroughputCutoffs extends S.Class( /** * Maximum p50 latency (seconds) */ - p50: S.optionalWith(S.Number, { nullable: true }), + p50: S.optional(S.NullOr(S.Number)), /** * Maximum p75 latency (seconds) */ - p75: S.optionalWith(S.Number, { nullable: true }), + p75: S.optional(S.NullOr(S.Number)), /** * Maximum p90 latency (seconds) */ - p90: S.optionalWith(S.Number, { nullable: true }), + p90: S.optional(S.NullOr(S.Number)), /** * Maximum p99 latency (seconds) */ - p99: S.optionalWith(S.Number, { nullable: true }), + p99: S.optional(S.NullOr(S.Number)), }) {} /** * Preferred maximum latency (in seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints above the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. */ -export class PreferredMaxLatency extends S.Union(S.Number, PercentileLatencyCutoffs) {} +export const PreferredMaxLatency = S.Union([S.Number, PercentileLatencyCutoffs]) /** * The search engine to use for web search. */ -export class WebSearchEngine extends S.Literal("native", "exa") {} +export const WebSearchEngine = S.Literals(["native", "exa"]) /** * The engine to use for parsing PDF files. */ -export class PDFParserEngine extends S.Literal("mistral-ocr", "pdf-text", "native") {} +export const PDFParserEngine = S.Literals(["mistral-ocr", "pdf-text", "native"]) /** * Options for PDF parsing. */ export class PDFParserOptions extends S.Class("PDFParserOptions")({ - engine: S.optionalWith(PDFParserEngine, { nullable: true }), + engine: S.optional(S.NullOr(PDFParserEngine)), }) {} /** * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). */ -export class OpenResponsesRequestRoute extends S.Literal("fallback", "sort") {} +export const OpenResponsesRequestRoute = S.Literals(["fallback", "sort"]) /** * Request schema for Responses endpoint */ export class OpenResponsesRequest extends S.Class("OpenResponsesRequest")({ - input: S.optionalWith(OpenResponsesInput, { nullable: true }), - instructions: S.optionalWith(S.String, { nullable: true }), - metadata: S.optionalWith(OpenResponsesRequestMetadata, { nullable: true }), - tools: S.optionalWith( - S.Array( - S.Union( + input: S.optional(S.NullOr(OpenResponsesInput)), + instructions: S.optional(S.NullOr(S.String)), + metadata: S.optional(S.NullOr(OpenResponsesRequestMetadata)), + tools: S.optional(S.NullOr(S.Array( + S.Union([ /** * Function tool definition */ @@ -906,131 +861,111 @@ export class OpenResponsesRequest extends S.Class("OpenRes OpenResponsesWebSearchPreview20250311Tool, OpenResponsesWebSearchTool, OpenResponsesWebSearch20250826Tool, - ), - ), - { nullable: true }, - ), - tool_choice: S.optionalWith(OpenAIResponsesToolChoice, { nullable: true }), - parallel_tool_calls: S.optionalWith(S.Boolean, { nullable: true }), - model: S.optionalWith(S.String, { nullable: true }), - models: S.optionalWith(S.Array(S.String), { nullable: true }), - text: S.optionalWith(OpenResponsesResponseText, { nullable: true }), - reasoning: S.optionalWith(OpenResponsesReasoningConfig, { nullable: true }), - max_output_tokens: S.optionalWith(S.Number, { nullable: true }), - temperature: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2)), { - nullable: true, - }), - top_p: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }), - top_logprobs: S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(20)), { - nullable: true, - }), - max_tool_calls: S.optionalWith(S.Int, { nullable: true }), - presence_penalty: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { - nullable: true, - }), - frequency_penalty: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { - nullable: true, - }), - top_k: S.optionalWith(S.Number, { nullable: true }), + ]), + ))), + tool_choice: S.optional(S.NullOr(OpenAIResponsesToolChoice)), + parallel_tool_calls: S.optional(S.NullOr(S.Boolean)), + model: S.optional(S.NullOr(S.String)), + models: S.optional(S.NullOr(S.Array(S.String))), + text: S.optional(S.NullOr(OpenResponsesResponseText)), + reasoning: S.optional(S.NullOr(OpenResponsesReasoningConfig)), + max_output_tokens: S.optional(S.NullOr(S.Number)), + temperature: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(2)))), + top_p: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0)))), + top_logprobs: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(20)))), + max_tool_calls: S.optional(S.NullOr(S.Int)), + presence_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), + frequency_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), + top_k: S.optional(S.NullOr(S.Number)), /** * Provider-specific image configuration options. Keys and values vary by model/provider. See https://openrouter.ai/docs/features/multimodal/image-generation for more details. */ - image_config: S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + image_config: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), /** * Output modalities for the response. Supported values are "text" and "image". */ - modalities: S.optionalWith(S.Array(ResponsesOutputModality), { nullable: true }), - prompt_cache_key: S.optionalWith(S.String, { nullable: true }), - previous_response_id: S.optionalWith(S.String, { nullable: true }), - prompt: S.optionalWith(OpenAIResponsesPrompt, { nullable: true }), - include: S.optionalWith(S.Array(OpenAIResponsesIncludable), { nullable: true }), - background: S.optionalWith(S.Boolean, { nullable: true }), - safety_identifier: S.optionalWith(S.String, { nullable: true }), - store: S.optionalWith(S.Literal(false), { nullable: true, default: () => false as const }), - service_tier: S.optionalWith(OpenResponsesRequestServiceTier, { - nullable: true, - default: () => "auto" as const, - }), - truncation: S.optionalWith(OpenResponsesRequestTruncation, { nullable: true }), - stream: S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), + modalities: S.optional(S.NullOr(S.Array(ResponsesOutputModality))), + prompt_cache_key: S.optional(S.NullOr(S.String)), + previous_response_id: S.optional(S.NullOr(S.String)), + prompt: S.optional(S.NullOr(OpenAIResponsesPrompt)), + include: S.optional(S.NullOr(S.Array(OpenAIResponsesIncludable))), + background: S.optional(S.NullOr(S.Boolean)), + safety_identifier: S.optional(S.NullOr(S.String)), + store: S.NullOr(S.Literal(false)).pipe(S.optional, S.withDecodingDefault(() => false as const)), + service_tier: S.NullOr(OpenResponsesRequestServiceTier).pipe(S.optional, S.withDecodingDefault(() => "auto" as const)), + truncation: S.optional(S.NullOr(OpenResponsesRequestTruncation)), + stream: S.NullOr(S.Boolean).pipe(S.optional, S.withDecodingDefault(() => false as const)), /** * When multiple model providers are available, optionally indicate your routing preference. */ - provider: S.optionalWith( - S.Struct({ + provider: S.optional(S.NullOr(S.Struct({ /** * Whether to allow backup providers to serve requests * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. */ - allow_fallbacks: S.optionalWith(S.Boolean, { nullable: true }), + allow_fallbacks: S.optional(S.NullOr(S.Boolean)), /** * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. */ - require_parameters: S.optionalWith(S.Boolean, { nullable: true }), - data_collection: S.optionalWith(DataCollection, { nullable: true }), + require_parameters: S.optional(S.NullOr(S.Boolean)), + data_collection: S.optional(S.NullOr(DataCollection)), /** * Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used. */ - zdr: S.optionalWith(S.Boolean, { nullable: true }), + zdr: S.optional(S.NullOr(S.Boolean)), /** * Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used. */ - enforce_distillable_text: S.optionalWith(S.Boolean, { nullable: true }), + enforce_distillable_text: S.optional(S.NullOr(S.Boolean)), /** * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. */ - order: S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + order: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), /** * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. */ - only: S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + only: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), /** * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. */ - ignore: S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + ignore: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), /** * A list of quantization levels to filter the provider by. */ - quantizations: S.optionalWith(S.Array(Quantization), { nullable: true }), + quantizations: S.optional(S.NullOr(S.Array(Quantization))), /** * The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. */ - sort: S.optionalWith(S.Union(ProviderSort, ProviderSortConfig), { nullable: true }), + sort: S.optional(S.NullOr(S.Union([ProviderSort, ProviderSortConfig]))), /** * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. */ - max_price: S.optionalWith( - S.Struct({ - prompt: S.optionalWith(BigNumberUnion, { nullable: true }), - completion: S.optionalWith(BigNumberUnion, { nullable: true }), - image: S.optionalWith(BigNumberUnion, { nullable: true }), - audio: S.optionalWith(BigNumberUnion, { nullable: true }), - request: S.optionalWith(BigNumberUnion, { nullable: true }), - }), - { nullable: true }, - ), - preferred_min_throughput: S.optionalWith(PreferredMinThroughput, { nullable: true }), - preferred_max_latency: S.optionalWith(PreferredMaxLatency, { nullable: true }), - }), - { nullable: true }, - ), + max_price: S.optional(S.Struct({ + prompt: S.optional(S.NullOr(BigNumberUnion)), + completion: S.optional(S.NullOr(BigNumberUnion)), + image: S.optional(S.NullOr(BigNumberUnion)), + audio: S.optional(S.NullOr(BigNumberUnion)), + request: S.optional(S.NullOr(BigNumberUnion)), + })), + preferred_min_throughput: S.optional(S.NullOr(PreferredMinThroughput)), + preferred_max_latency: S.optional(S.NullOr(PreferredMaxLatency)), + }))), /** * Plugins you want to enable for this request, including their settings. */ - plugins: S.optionalWith( - S.Array( - S.Union( + plugins: S.optional(S.NullOr(S.Array( + S.Union([ S.Struct({ id: S.Literal("auto-router"), /** * Set to false to disable the auto-router plugin for this request. Defaults to true. */ - enabled: S.optionalWith(S.Boolean, { nullable: true }), + enabled: S.optional(S.NullOr(S.Boolean)), /** * List of model patterns to filter which models the auto-router can route between. Supports wildcards (e.g., "anthropic/*" matches all Anthropic models). When not specified, uses the default supported models list. */ - allowed_models: S.optionalWith(S.Array(S.String), { nullable: true }), + allowed_models: S.optional(S.NullOr(S.Array(S.String))), }), S.Struct({ id: S.Literal("moderation"), @@ -1040,100 +975,87 @@ export class OpenResponsesRequest extends S.Class("OpenRes /** * Set to false to disable the web-search plugin for this request. Defaults to true. */ - enabled: S.optionalWith(S.Boolean, { nullable: true }), - max_results: S.optionalWith(S.Number, { nullable: true }), - search_prompt: S.optionalWith(S.String, { nullable: true }), - engine: S.optionalWith(WebSearchEngine, { nullable: true }), + enabled: S.optional(S.NullOr(S.Boolean)), + max_results: S.optional(S.NullOr(S.Number)), + search_prompt: S.optional(S.NullOr(S.String)), + engine: S.optional(S.NullOr(WebSearchEngine)), }), S.Struct({ id: S.Literal("file-parser"), /** * Set to false to disable the file-parser plugin for this request. Defaults to true. */ - enabled: S.optionalWith(S.Boolean, { nullable: true }), - pdf: S.optionalWith(PDFParserOptions, { nullable: true }), + enabled: S.optional(S.NullOr(S.Boolean)), + pdf: S.optional(S.NullOr(PDFParserOptions)), }), S.Struct({ id: S.Literal("response-healing"), /** * Set to false to disable the response-healing plugin for this request. Defaults to true. */ - enabled: S.optionalWith(S.Boolean, { nullable: true }), + enabled: S.optional(S.NullOr(S.Boolean)), }), - ), - ), - { nullable: true }, - ), + ]), + ))), /** * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). */ - route: S.optionalWith(OpenResponsesRequestRoute, { nullable: true }), + route: S.optional(S.NullOr(OpenResponsesRequestRoute)), /** * A unique identifier representing your end-user, which helps distinguish between different users of your app. This allows your app to identify specific users in case of abuse reports, preventing your entire app from being affected by the actions of individual users. Maximum of 128 characters. */ - user: S.optionalWith(S.String.pipe(S.maxLength(128)), { nullable: true }), + user: S.optional(S.NullOr(S.String.check(S.isMaxLength(128)))), /** * A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters. */ - session_id: S.optionalWith(S.String.pipe(S.maxLength(128)), { nullable: true }), + session_id: S.optional(S.NullOr(S.String.check(S.isMaxLength(128)))), }) {} -export class OutputMessageRole extends S.Literal("assistant") {} +export const OutputMessageRole = S.Literal("assistant") -export class OutputMessageType extends S.Literal("message") {} +export const OutputMessageType = S.Literal("message") -export class OutputMessageStatusEnum extends S.Literal("in_progress") {} +export const OutputMessageStatusEnum = S.Literal("in_progress") export class OutputMessage extends S.Class("OutputMessage")({ id: S.String, role: OutputMessageRole, type: OutputMessageType, - status: S.optionalWith( - S.Union(OutputMessageStatusEnum, OutputMessageStatusEnum, OutputMessageStatusEnum), - { - nullable: true, - }, - ), - content: S.Array(S.Union(ResponseOutputText, OpenAIResponsesRefusalContent)), + status: S.optional(S.NullOr(S.Union([OutputMessageStatusEnum, OutputMessageStatusEnum, OutputMessageStatusEnum]))), + content: S.Array(S.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), }) {} -export class OutputItemReasoningType extends S.Literal("reasoning") {} +export const OutputItemReasoningType = S.Literal("reasoning") -export class OutputItemReasoningStatusEnum extends S.Literal("in_progress") {} +export const OutputItemReasoningStatusEnum = S.Literal("in_progress") export class OutputItemReasoning extends S.Class("OutputItemReasoning")({ type: OutputItemReasoningType, id: S.String, - content: S.optionalWith(S.Array(ReasoningTextContent), { nullable: true }), + content: S.optional(S.NullOr(S.Array(ReasoningTextContent))), summary: S.Array(ReasoningSummaryText), - encrypted_content: S.optionalWith(S.String, { nullable: true }), - status: S.optionalWith( - S.Union(OutputItemReasoningStatusEnum, OutputItemReasoningStatusEnum, OutputItemReasoningStatusEnum), - { nullable: true }, - ), + encrypted_content: S.optional(S.NullOr(S.String)), + status: S.optional(S.NullOr(S.Union([OutputItemReasoningStatusEnum, OutputItemReasoningStatusEnum, OutputItemReasoningStatusEnum]))), }) {} -export class OutputItemFunctionCallType extends S.Literal("function_call") {} +export const OutputItemFunctionCallType = S.Literal("function_call") -export class OutputItemFunctionCallStatusEnum extends S.Literal("in_progress") {} +export const OutputItemFunctionCallStatusEnum = S.Literal("in_progress") export class OutputItemFunctionCall extends S.Class("OutputItemFunctionCall")({ type: OutputItemFunctionCallType, - id: S.optionalWith(S.String, { nullable: true }), + id: S.optional(S.NullOr(S.String)), name: S.String, arguments: S.String, call_id: S.String, - status: S.optionalWith( - S.Union( + status: S.optional(S.NullOr(S.Union([ OutputItemFunctionCallStatusEnum, OutputItemFunctionCallStatusEnum, OutputItemFunctionCallStatusEnum, - ), - { nullable: true }, - ), + ]))), }) {} -export class OutputItemWebSearchCallType extends S.Literal("web_search_call") {} +export const OutputItemWebSearchCallType = S.Literal("web_search_call") export class OutputItemWebSearchCall extends S.Class("OutputItemWebSearchCall")({ type: OutputItemWebSearchCallType, @@ -1141,7 +1063,7 @@ export class OutputItemWebSearchCall extends S.Class("O status: WebSearchStatus, }) {} -export class OutputItemFileSearchCallType extends S.Literal("file_search_call") {} +export const OutputItemFileSearchCallType = S.Literal("file_search_call") export class OutputItemFileSearchCall extends S.Class("OutputItemFileSearchCall")({ type: OutputItemFileSearchCallType, @@ -1150,14 +1072,14 @@ export class OutputItemFileSearchCall extends S.Class( status: WebSearchStatus, }) {} -export class OutputItemImageGenerationCallType extends S.Literal("image_generation_call") {} +export const OutputItemImageGenerationCallType = S.Literal("image_generation_call") export class OutputItemImageGenerationCall extends S.Class( "OutputItemImageGenerationCall", )({ type: OutputItemImageGenerationCallType, id: S.String, - result: S.optionalWith(S.NullOr(S.String), { default: () => null }), + result: S.NullOr(S.String).pipe(S.optional, S.withDecodingDefault(() => null)), status: ImageGenerationStatus, }) {} @@ -1173,19 +1095,16 @@ export class OpenAIResponsesUsage extends S.Class("OpenAIR total_tokens: S.Number, }) {} -export class OpenResponsesNonStreamingResponseObject extends S.Literal("response") {} +export const OpenResponsesNonStreamingResponseObject = S.Literal("response") -export class OpenAIResponsesResponseStatus extends S.Literal( - "completed", +export const OpenAIResponsesResponseStatus = S.Literals(["completed", "incomplete", "in_progress", "failed", "cancelled", - "queued", -) {} + "queued",]) -export class ResponsesErrorFieldCode extends S.Literal( - "server_error", +export const ResponsesErrorFieldCode = S.Literals(["server_error", "rate_limit_exceeded", "invalid_prompt", "vector_store_timeout", @@ -1202,8 +1121,7 @@ export class ResponsesErrorFieldCode extends S.Literal( "unsupported_image_media_type", "empty_image_file", "failed_to_download_image", - "image_file_not_found", -) {} + "image_file_not_found",]) /** * Error information returned from the API @@ -1213,20 +1131,18 @@ export class ResponsesErrorField extends S.Class("Responses message: S.String, }) {} -export class OpenAIResponsesIncompleteDetailsReason extends S.Literal( - "max_output_tokens", - "content_filter", -) {} +export const OpenAIResponsesIncompleteDetailsReason = S.Literals(["max_output_tokens", + "content_filter",]) export class OpenAIResponsesIncompleteDetails extends S.Class( "OpenAIResponsesIncompleteDetails", )({ - reason: S.optionalWith(OpenAIResponsesIncompleteDetailsReason, { nullable: true }), + reason: S.optional(S.NullOr(OpenAIResponsesIncompleteDetailsReason)), }) {} -export class ResponseInputImageType extends S.Literal("input_image") {} +export const ResponseInputImageType = S.Literal("input_image") -export class ResponseInputImageDetail extends S.Literal("auto", "high", "low") {} +export const ResponseInputImageDetail = S.Literals(["auto", "high", "low"]) /** * Image input content item @@ -1234,107 +1150,105 @@ export class ResponseInputImageDetail extends S.Literal("auto", "high", "low") { export class ResponseInputImage extends S.Class("ResponseInputImage")({ type: ResponseInputImageType, detail: ResponseInputImageDetail, - image_url: S.optionalWith(S.String, { nullable: true }), + image_url: S.optional(S.NullOr(S.String)), }) {} -export class OpenAIResponsesInput extends S.Union( - S.String, +export const OpenAIResponsesInput = S.Union([S.String, S.Array( - S.Union( + S.Union([ S.Struct({ - type: S.optionalWith(S.Literal("message"), { nullable: true }), - role: S.Union( + type: S.optional(S.NullOr(S.Literal("message"))), + role: S.Union([ S.Literal("user"), S.Literal("system"), S.Literal("assistant"), S.Literal("developer"), - ), - content: S.Union( + ]), + content: S.Union([ S.Array( - S.Union(ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio), + S.Union([ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio]), ), S.String, - ), + ]), }), S.Struct({ id: S.String, - type: S.optionalWith(S.Literal("message"), { nullable: true }), - role: S.Union(S.Literal("user"), S.Literal("system"), S.Literal("developer")), + type: S.optional(S.NullOr(S.Literal("message"))), + role: S.Union([S.Literal("user"), S.Literal("system"), S.Literal("developer")]), content: S.Array( - S.Union(ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio), + S.Union([ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio]), ), }), S.Struct({ type: S.Literal("function_call_output"), - id: S.optionalWith(S.String, { nullable: true }), + id: S.optional(S.NullOr(S.String)), call_id: S.String, output: S.String, - status: S.optionalWith(ToolCallStatus, { nullable: true }), + status: S.optional(S.NullOr(ToolCallStatus)), }), S.Struct({ type: S.Literal("function_call"), call_id: S.String, name: S.String, arguments: S.String, - id: S.optionalWith(S.String, { nullable: true }), - status: S.optionalWith(ToolCallStatus, { nullable: true }), + id: S.optional(S.NullOr(S.String)), + status: S.optional(S.NullOr(ToolCallStatus)), }), OutputItemImageGenerationCall, OutputMessage, - ), - ), -) {} + ]), + ),]) export class OpenAIResponsesReasoningConfig extends S.Class( "OpenAIResponsesReasoningConfig", )({ - effort: S.optionalWith(OpenAIResponsesReasoningEffort, { nullable: true }), - summary: S.optionalWith(ReasoningSummaryVerbosity, { nullable: true }), + effort: S.optional(S.NullOr(OpenAIResponsesReasoningEffort)), + summary: S.optional(S.NullOr(ReasoningSummaryVerbosity)), }) {} -export class OpenAIResponsesServiceTier extends S.Literal("auto", "default", "flex", "priority", "scale") {} +export const OpenAIResponsesServiceTier = S.Literals(["auto", "default", "flex", "priority", "scale"]) -export class OpenAIResponsesTruncation extends S.Literal("auto", "disabled") {} +export const OpenAIResponsesTruncation = S.Literals(["auto", "disabled"]) -export class ResponseTextConfigVerbosity extends S.Literal("high", "low", "medium") {} +export const ResponseTextConfigVerbosity = S.Literals(["high", "low", "medium"]) /** * Text output configuration including format and verbosity */ export class ResponseTextConfig extends S.Class("ResponseTextConfig")({ - format: S.optionalWith(ResponseFormatTextConfig, { nullable: true }), - verbosity: S.optionalWith(ResponseTextConfigVerbosity, { nullable: true }), + format: S.optional(S.NullOr(ResponseFormatTextConfig)), + verbosity: S.optional(S.NullOr(ResponseTextConfigVerbosity)), }) {} export class OpenResponsesNonStreamingResponse extends S.Class( "OpenResponsesNonStreamingResponse", )({ output: S.Array( - S.Union( + S.Union([ OutputMessage, OutputItemReasoning, OutputItemFunctionCall, OutputItemWebSearchCall, OutputItemFileSearchCall, OutputItemImageGenerationCall, - ), + ]), ), - usage: S.optionalWith(OpenAIResponsesUsage, { nullable: true }), + usage: S.optional(S.NullOr(OpenAIResponsesUsage)), id: S.String, object: OpenResponsesNonStreamingResponseObject, created_at: S.Number, model: S.String, status: OpenAIResponsesResponseStatus, completed_at: S.NullOr(S.Number), - user: S.optionalWith(S.String, { nullable: true }), - output_text: S.optionalWith(S.String, { nullable: true }), - prompt_cache_key: S.optionalWith(S.String, { nullable: true }), - safety_identifier: S.optionalWith(S.String, { nullable: true }), + user: S.optional(S.NullOr(S.String)), + output_text: S.optional(S.NullOr(S.String)), + prompt_cache_key: S.optional(S.NullOr(S.String)), + safety_identifier: S.optional(S.NullOr(S.String)), error: S.NullOr(ResponsesErrorField), incomplete_details: S.NullOr(OpenAIResponsesIncompleteDetails), - max_tool_calls: S.optionalWith(S.Number, { nullable: true }), - top_logprobs: S.optionalWith(S.Number, { nullable: true }), - max_output_tokens: S.optionalWith(S.Number, { nullable: true }), + max_tool_calls: S.optional(S.NullOr(S.Number)), + top_logprobs: S.optional(S.NullOr(S.Number)), + max_output_tokens: S.optional(S.NullOr(S.Number)), temperature: S.NullOr(S.Number), top_p: S.NullOr(S.Number), presence_penalty: S.NullOr(S.Number), @@ -1342,7 +1256,7 @@ export class OpenResponsesNonStreamingResponse extends S.Class("BadRequestResponse")({ error: BadRequestResponseErrorData, - user_id: S.optionalWith(S.String, { nullable: true }), + user_id: S.optional(S.NullOr(S.String)), }) {} /** @@ -1392,7 +1306,7 @@ export class UnauthorizedResponseErrorData extends S.Class("UnauthorizedResponse")({ error: UnauthorizedResponseErrorData, - user_id: S.optionalWith(S.String, { nullable: true }), + user_id: S.optional(S.NullOr(S.String)), }) {} /** @@ -1411,7 +1325,7 @@ export class PaymentRequiredResponseErrorData extends S.Class("PaymentRequiredResponse")({ error: PaymentRequiredResponseErrorData, - user_id: S.optionalWith(S.String, { nullable: true }), + user_id: S.optional(S.NullOr(S.String)), }) {} /** @@ -1430,7 +1344,7 @@ export class NotFoundResponseErrorData extends S.Class("NotFoundResponse")({ error: NotFoundResponseErrorData, - user_id: S.optionalWith(S.String, { nullable: true }), + user_id: S.optional(S.NullOr(S.String)), }) {} /** @@ -1449,7 +1363,7 @@ export class RequestTimeoutResponseErrorData extends S.Class("RequestTimeoutResponse")({ error: RequestTimeoutResponseErrorData, - user_id: S.optionalWith(S.String, { nullable: true }), + user_id: S.optional(S.NullOr(S.String)), }) {} /** @@ -1468,7 +1382,7 @@ export class PayloadTooLargeResponseErrorData extends S.Class("PayloadTooLargeResponse")({ error: PayloadTooLargeResponseErrorData, - user_id: S.optionalWith(S.String, { nullable: true }), + user_id: S.optional(S.NullOr(S.String)), }) {} /** @@ -1487,7 +1401,7 @@ export class UnprocessableEntityResponseErrorData extends S.Class("TooManyRequestsResponse")({ error: TooManyRequestsResponseErrorData, - user_id: S.optionalWith(S.String, { nullable: true }), + user_id: S.optional(S.NullOr(S.String)), }) {} /** @@ -1527,7 +1441,7 @@ export class InternalServerResponseErrorData extends S.Class("InternalServerResponse")({ error: InternalServerResponseErrorData, - user_id: S.optionalWith(S.String, { nullable: true }), + user_id: S.optional(S.NullOr(S.String)), }) {} /** @@ -1546,7 +1460,7 @@ export class BadGatewayResponseErrorData extends S.Class("BadGatewayResponse")({ error: BadGatewayResponseErrorData, - user_id: S.optionalWith(S.String, { nullable: true }), + user_id: S.optional(S.NullOr(S.String)), }) {} /** @@ -1565,7 +1479,7 @@ export class ServiceUnavailableResponseErrorData extends S.Class( model: S.String, max_tokens: S.Number, messages: S.Array(OpenRouterAnthropicMessageParam), - system: S.optionalWith( - S.Union( + system: S.optional(S.NullOr(S.Union([ S.String, S.Array( S.Struct({ type: S.Literal("text"), text: S.String, - citations: S.optionalWith( - S.Array( - S.Union( + citations: S.optional(S.Array( + S.Union([ S.Struct({ type: S.Literal("char_location"), cited_text: S.String, @@ -2175,111 +2019,82 @@ export class AnthropicMessagesRequest extends S.Class( start_block_index: S.Number, end_block_index: S.Number, }), - ), - ), - { nullable: true }, - ), - cache_control: S.optionalWith( - S.Struct({ + ]), + )), + cache_control: S.optional(S.Struct({ type: S.Literal("ephemeral"), - ttl: S.optionalWith(S.Literal("5m", "1h"), { nullable: true }), - }), - { nullable: true }, - ), + ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), + })), }), ), - ), - { nullable: true }, - ), - metadata: S.optionalWith( - S.Struct({ - user_id: S.optionalWith(S.String, { nullable: true }), - }), - { nullable: true }, - ), - stop_sequences: S.optionalWith(S.Array(S.String), { nullable: true }), - stream: S.optionalWith(S.Boolean, { nullable: true }), - temperature: S.optionalWith(S.Number, { nullable: true }), - top_p: S.optionalWith(S.Number, { nullable: true }), - top_k: S.optionalWith(S.Number, { nullable: true }), - tools: S.optionalWith( - S.Array( - S.Union( + ]))), + metadata: S.optional(S.NullOr(S.Struct({ + user_id: S.optional(S.NullOr(S.String)), + }))), + stop_sequences: S.optional(S.NullOr(S.Array(S.String))), + stream: S.optional(S.NullOr(S.Boolean)), + temperature: S.optional(S.NullOr(S.Number)), + top_p: S.optional(S.NullOr(S.Number)), + top_k: S.optional(S.NullOr(S.Number)), + tools: S.optional(S.NullOr(S.Array( + S.Union([ S.Struct({ name: S.String, - description: S.optionalWith(S.String, { nullable: true }), + description: S.optional(S.NullOr(S.String)), input_schema: S.Struct({ type: S.Literal("object"), - required: S.optionalWith(S.Array(S.String), { nullable: true }), + required: S.optional(S.NullOr(S.Array(S.String))), }), - type: S.optionalWith(S.Literal("custom"), { nullable: true }), - cache_control: S.optionalWith( - S.Struct({ + type: S.optional(S.NullOr(S.Literal("custom"))), + cache_control: S.optional(S.Struct({ type: S.Literal("ephemeral"), - ttl: S.optionalWith(S.Literal("5m", "1h"), { nullable: true }), - }), - { nullable: true }, - ), + ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), + })), }), S.Struct({ type: S.Literal("bash_20250124"), name: S.Literal("bash"), - cache_control: S.optionalWith( - S.Struct({ + cache_control: S.optional(S.Struct({ type: S.Literal("ephemeral"), - ttl: S.optionalWith(S.Literal("5m", "1h"), { nullable: true }), - }), - { nullable: true }, - ), + ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), + })), }), S.Struct({ type: S.Literal("text_editor_20250124"), name: S.Literal("str_replace_editor"), - cache_control: S.optionalWith( - S.Struct({ + cache_control: S.optional(S.Struct({ type: S.Literal("ephemeral"), - ttl: S.optionalWith(S.Literal("5m", "1h"), { nullable: true }), - }), - { nullable: true }, - ), + ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), + })), }), S.Struct({ type: S.Literal("web_search_20250305"), name: S.Literal("web_search"), - allowed_domains: S.optionalWith(S.Array(S.String), { nullable: true }), - blocked_domains: S.optionalWith(S.Array(S.String), { nullable: true }), - max_uses: S.optionalWith(S.Number, { nullable: true }), - user_location: S.optionalWith( - S.Struct({ + allowed_domains: S.optional(S.NullOr(S.Array(S.String))), + blocked_domains: S.optional(S.NullOr(S.Array(S.String))), + max_uses: S.optional(S.NullOr(S.Number)), + user_location: S.optional(S.Struct({ type: S.Literal("approximate"), - city: S.optionalWith(S.String, { nullable: true }), - country: S.optionalWith(S.String, { nullable: true }), - region: S.optionalWith(S.String, { nullable: true }), - timezone: S.optionalWith(S.String, { nullable: true }), - }), - { nullable: true }, - ), - cache_control: S.optionalWith( - S.Struct({ + city: S.optional(S.NullOr(S.String)), + country: S.optional(S.NullOr(S.String)), + region: S.optional(S.NullOr(S.String)), + timezone: S.optional(S.NullOr(S.String)), + })), + cache_control: S.optional(S.Struct({ type: S.Literal("ephemeral"), - ttl: S.optionalWith(S.Literal("5m", "1h"), { nullable: true }), - }), - { nullable: true }, - ), + ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), + })), }), - ), - ), - { nullable: true }, - ), - tool_choice: S.optionalWith( - S.Union( + ]), + ))), + tool_choice: S.optional(S.NullOr(S.Union([ S.Struct({ type: AnthropicMessagesRequestToolChoiceEnumType, - disable_parallel_tool_use: S.optionalWith(S.Boolean, { nullable: true }), + disable_parallel_tool_use: S.optional(S.NullOr(S.Boolean)), }), S.Struct({ type: AnthropicMessagesRequestToolChoiceEnumType, - disable_parallel_tool_use: S.optionalWith(S.Boolean, { nullable: true }), + disable_parallel_tool_use: S.optional(S.NullOr(S.Boolean)), }), S.Struct({ type: AnthropicMessagesRequestToolChoiceEnumType, @@ -2287,13 +2102,10 @@ export class AnthropicMessagesRequest extends S.Class( S.Struct({ type: AnthropicMessagesRequestToolChoiceEnumType, name: S.String, - disable_parallel_tool_use: S.optionalWith(S.Boolean, { nullable: true }), + disable_parallel_tool_use: S.optional(S.NullOr(S.Boolean)), }), - ), - { nullable: true }, - ), - thinking: S.optionalWith( - S.Union( + ]))), + thinking: S.optional(S.NullOr(S.Union([ S.Struct({ type: AnthropicMessagesRequestThinkingEnumType, budget_tokens: S.Number, @@ -2301,85 +2113,76 @@ export class AnthropicMessagesRequest extends S.Class( S.Struct({ type: AnthropicMessagesRequestThinkingEnumType, }), - ), - { nullable: true }, - ), - service_tier: S.optionalWith(AnthropicMessagesRequestServiceTier, { nullable: true }), + ]))), + service_tier: S.optional(S.NullOr(AnthropicMessagesRequestServiceTier)), /** * When multiple model providers are available, optionally indicate your routing preference. */ - provider: S.optionalWith( - S.Struct({ + provider: S.optional(S.NullOr(S.Struct({ /** * Whether to allow backup providers to serve requests * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. */ - allow_fallbacks: S.optionalWith(S.Boolean, { nullable: true }), + allow_fallbacks: S.optional(S.NullOr(S.Boolean)), /** * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. */ - require_parameters: S.optionalWith(S.Boolean, { nullable: true }), - data_collection: S.optionalWith(DataCollection, { nullable: true }), + require_parameters: S.optional(S.NullOr(S.Boolean)), + data_collection: S.optional(S.NullOr(DataCollection)), /** * Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used. */ - zdr: S.optionalWith(S.Boolean, { nullable: true }), + zdr: S.optional(S.NullOr(S.Boolean)), /** * Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used. */ - enforce_distillable_text: S.optionalWith(S.Boolean, { nullable: true }), + enforce_distillable_text: S.optional(S.NullOr(S.Boolean)), /** * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. */ - order: S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + order: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), /** * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. */ - only: S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + only: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), /** * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. */ - ignore: S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + ignore: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), /** * A list of quantization levels to filter the provider by. */ - quantizations: S.optionalWith(S.Array(Quantization), { nullable: true }), - sort: S.optionalWith(AnthropicMessagesRequestProviderSort, { nullable: true }), + quantizations: S.optional(S.NullOr(S.Array(Quantization))), + sort: S.optional(S.NullOr(AnthropicMessagesRequestProviderSort)), /** * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. */ - max_price: S.optionalWith( - S.Struct({ - prompt: S.optionalWith(BigNumberUnion, { nullable: true }), - completion: S.optionalWith(BigNumberUnion, { nullable: true }), - image: S.optionalWith(BigNumberUnion, { nullable: true }), - audio: S.optionalWith(BigNumberUnion, { nullable: true }), - request: S.optionalWith(BigNumberUnion, { nullable: true }), - }), - { nullable: true }, - ), - preferred_min_throughput: S.optionalWith(PreferredMinThroughput, { nullable: true }), - preferred_max_latency: S.optionalWith(PreferredMaxLatency, { nullable: true }), - }), - { nullable: true }, - ), + max_price: S.optional(S.Struct({ + prompt: S.optional(S.NullOr(BigNumberUnion)), + completion: S.optional(S.NullOr(BigNumberUnion)), + image: S.optional(S.NullOr(BigNumberUnion)), + audio: S.optional(S.NullOr(BigNumberUnion)), + request: S.optional(S.NullOr(BigNumberUnion)), + })), + preferred_min_throughput: S.optional(S.NullOr(PreferredMinThroughput)), + preferred_max_latency: S.optional(S.NullOr(PreferredMaxLatency)), + }))), /** * Plugins you want to enable for this request, including their settings. */ - plugins: S.optionalWith( - S.Array( - S.Union( + plugins: S.optional(S.NullOr(S.Array( + S.Union([ S.Struct({ id: S.Literal("auto-router"), /** * Set to false to disable the auto-router plugin for this request. Defaults to true. */ - enabled: S.optionalWith(S.Boolean, { nullable: true }), + enabled: S.optional(S.NullOr(S.Boolean)), /** * List of model patterns to filter which models the auto-router can route between. Supports wildcards (e.g., "anthropic/*" matches all Anthropic models). When not specified, uses the default supported models list. */ - allowed_models: S.optionalWith(S.Array(S.String), { nullable: true }), + allowed_models: S.optional(S.NullOr(S.Array(S.String))), }), S.Struct({ id: S.Literal("moderation"), @@ -2389,60 +2192,56 @@ export class AnthropicMessagesRequest extends S.Class( /** * Set to false to disable the web-search plugin for this request. Defaults to true. */ - enabled: S.optionalWith(S.Boolean, { nullable: true }), - max_results: S.optionalWith(S.Number, { nullable: true }), - search_prompt: S.optionalWith(S.String, { nullable: true }), - engine: S.optionalWith(WebSearchEngine, { nullable: true }), + enabled: S.optional(S.NullOr(S.Boolean)), + max_results: S.optional(S.NullOr(S.Number)), + search_prompt: S.optional(S.NullOr(S.String)), + engine: S.optional(S.NullOr(WebSearchEngine)), }), S.Struct({ id: S.Literal("file-parser"), /** * Set to false to disable the file-parser plugin for this request. Defaults to true. */ - enabled: S.optionalWith(S.Boolean, { nullable: true }), - pdf: S.optionalWith(PDFParserOptions, { nullable: true }), + enabled: S.optional(S.NullOr(S.Boolean)), + pdf: S.optional(S.NullOr(PDFParserOptions)), }), S.Struct({ id: S.Literal("response-healing"), /** * Set to false to disable the response-healing plugin for this request. Defaults to true. */ - enabled: S.optionalWith(S.Boolean, { nullable: true }), + enabled: S.optional(S.NullOr(S.Boolean)), }), - ), - ), - { nullable: true }, - ), + ]), + ))), /** * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). */ - route: S.optionalWith(AnthropicMessagesRequestRoute, { nullable: true }), + route: S.optional(S.NullOr(AnthropicMessagesRequestRoute)), /** * A unique identifier representing your end-user, which helps distinguish between different users of your app. This allows your app to identify specific users in case of abuse reports, preventing your entire app from being affected by the actions of individual users. Maximum of 128 characters. */ - user: S.optionalWith(S.String.pipe(S.maxLength(128)), { nullable: true }), + user: S.optional(S.NullOr(S.String.check(S.isMaxLength(128)))), /** * A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters. */ - session_id: S.optionalWith(S.String.pipe(S.maxLength(128)), { nullable: true }), - models: S.optionalWith(S.Array(S.String), { nullable: true }), + session_id: S.optional(S.NullOr(S.String.check(S.isMaxLength(128)))), + models: S.optional(S.NullOr(S.Array(S.String))), }) {} -export class AnthropicMessagesResponseType extends S.Literal("message") {} +export const AnthropicMessagesResponseType = S.Literal("message") -export class AnthropicMessagesResponseRole extends S.Literal("assistant") {} +export const AnthropicMessagesResponseRole = S.Literal("assistant") -export class AnthropicMessagesResponseStopReason extends S.Literal( - "end_turn", +export const AnthropicMessagesResponseStopReason = S.Literals(["end_turn", "max_tokens", "stop_sequence", "tool_use", "pause_turn", "refusal", - "model_context_window_exceeded", -) {} + "model_context_window_exceeded",]) -export class AnthropicMessagesResponseUsageServiceTier extends S.Literal("standard", "priority", "batch") {} +export const AnthropicMessagesResponseUsageServiceTier = S.Literals(["standard", "priority", "batch"]) export class AnthropicMessagesResponse extends S.Class( "AnthropicMessagesResponse", @@ -2451,13 +2250,13 @@ export class AnthropicMessagesResponse extends S.Class("ActivityItem")({ /** @@ -2708,12 +2507,12 @@ export class ActivityItem extends S.Class("ActivityItem")({ reasoning_tokens: S.Number, }) {} -export class GetUserActivity200 extends S.Struct({ +export const GetUserActivity200 = S.Struct({ /** * List of activity items */ data: S.Array(ActivityItem), -}) {} +}) /** * Error data for ForbiddenResponse @@ -2723,7 +2522,7 @@ export class ForbiddenResponseErrorData extends S.Class("ForbiddenResponse")({ error: ForbiddenResponseErrorData, - user_id: S.optionalWith(S.String, { nullable: true }), + user_id: S.optional(S.NullOr(S.String)), }) {} /** * Total credits purchased and used */ -export class GetCredits200 extends S.Struct({ +export const GetCredits200 = S.Struct({ data: S.Struct({ /** * Total credits purchased @@ -2748,9 +2547,9 @@ export class GetCredits200 extends S.Struct({ */ total_usage: S.Number, }), -}) {} +}) -export class CreateChargeRequestChainId extends S.Literal(1, 137, 8453) {} +export const CreateChargeRequestChainId = S.Literals([1, 137, 8453]) /** * Create a Coinbase charge for crypto payment @@ -2761,7 +2560,7 @@ export class CreateChargeRequest extends S.Class("CreateCha chain_id: CreateChargeRequestChainId, }) {} -export class CreateCoinbaseCharge200 extends S.Struct({ +export const CreateCoinbaseCharge200 = S.Struct({ data: S.Struct({ id: S.String, created_at: S.String, @@ -2788,14 +2587,14 @@ export class CreateCoinbaseCharge200 extends S.Struct({ }), }), }), -}) {} +}) -export class CreateEmbeddingsRequestEncodingFormat extends S.Literal("float", "base64") {} +export const CreateEmbeddingsRequestEncodingFormat = S.Literals(["float", "base64"]) /** * The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. */ -export class ProviderPreferencesSort extends S.Literal("price", "throughput", "latency") {} +export const ProviderPreferencesSort = S.Literals(["price", "throughput", "latency"]) /** * Provider routing preferences for the request. @@ -2806,56 +2605,53 @@ export class ProviderPreferences extends S.Class("ProviderP * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. */ - allow_fallbacks: S.optionalWith(S.Boolean, { nullable: true }), + allow_fallbacks: S.optional(S.NullOr(S.Boolean)), /** * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. */ - require_parameters: S.optionalWith(S.Boolean, { nullable: true }), - data_collection: S.optionalWith(DataCollection, { nullable: true }), + require_parameters: S.optional(S.NullOr(S.Boolean)), + data_collection: S.optional(S.NullOr(DataCollection)), /** * Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used. */ - zdr: S.optionalWith(S.Boolean, { nullable: true }), + zdr: S.optional(S.NullOr(S.Boolean)), /** * Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used. */ - enforce_distillable_text: S.optionalWith(S.Boolean, { nullable: true }), + enforce_distillable_text: S.optional(S.NullOr(S.Boolean)), /** * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. */ - order: S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + order: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), /** * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. */ - only: S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + only: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), /** * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. */ - ignore: S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + ignore: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), /** * A list of quantization levels to filter the provider by. */ - quantizations: S.optionalWith(S.Array(Quantization), { nullable: true }), - sort: S.optionalWith(ProviderPreferencesSort, { nullable: true }), + quantizations: S.optional(S.NullOr(S.Array(Quantization))), + sort: S.optional(S.NullOr(ProviderPreferencesSort)), /** * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. */ - max_price: S.optionalWith( - S.Struct({ - prompt: S.optionalWith(BigNumberUnion, { nullable: true }), - completion: S.optionalWith(BigNumberUnion, { nullable: true }), - image: S.optionalWith(BigNumberUnion, { nullable: true }), - audio: S.optionalWith(BigNumberUnion, { nullable: true }), - request: S.optionalWith(BigNumberUnion, { nullable: true }), - }), - { nullable: true }, - ), - preferred_min_throughput: S.optionalWith(PreferredMinThroughput, { nullable: true }), - preferred_max_latency: S.optionalWith(PreferredMaxLatency, { nullable: true }), + max_price: S.optional(S.NullOr(S.Struct({ + prompt: S.optional(S.NullOr(BigNumberUnion)), + completion: S.optional(S.NullOr(BigNumberUnion)), + image: S.optional(S.NullOr(BigNumberUnion)), + audio: S.optional(S.NullOr(BigNumberUnion)), + request: S.optional(S.NullOr(BigNumberUnion)), + }))), + preferred_min_throughput: S.optional(S.NullOr(PreferredMinThroughput)), + preferred_max_latency: S.optional(S.NullOr(PreferredMaxLatency)), }) {} export class CreateEmbeddingsRequest extends S.Class("CreateEmbeddingsRequest")({ - input: S.Union( + input: S.Union([ S.String, S.Array(S.String), S.Array(S.Number), @@ -2863,7 +2659,7 @@ export class CreateEmbeddingsRequest extends S.Class("C S.Array( S.Struct({ content: S.Array( - S.Union( + S.Union([ S.Struct({ type: S.Literal("text"), text: S.String, @@ -2874,41 +2670,38 @@ export class CreateEmbeddingsRequest extends S.Class("C url: S.String, }), }), - ), + ]), ), }), ), - ), + ]), model: S.String, - encoding_format: S.optionalWith(CreateEmbeddingsRequestEncodingFormat, { nullable: true }), - dimensions: S.optionalWith(S.Int.pipe(S.greaterThan(0)), { nullable: true }), - user: S.optionalWith(S.String, { nullable: true }), - provider: S.optionalWith(ProviderPreferences, { nullable: true }), - input_type: S.optionalWith(S.String, { nullable: true }), + encoding_format: S.optional(S.NullOr(CreateEmbeddingsRequestEncodingFormat)), + dimensions: S.optional(S.NullOr(S.Int.check(S.isGreaterThan(0)))), + user: S.optional(S.NullOr(S.String)), + provider: S.optional(S.NullOr(ProviderPreferences)), + input_type: S.optional(S.NullOr(S.String)), }) {} -export class CreateEmbeddings200Object extends S.Literal("list") {} +export const CreateEmbeddings200Object = S.Literal("list") -export class CreateEmbeddings200 extends S.Struct({ - id: S.optionalWith(S.String, { nullable: true }), +export const CreateEmbeddings200 = S.Struct({ + id: S.optional(S.NullOr(S.String)), object: CreateEmbeddings200Object, data: S.Array( S.Struct({ object: S.Literal("embedding"), - embedding: S.Union(S.Array(S.Number), S.String), - index: S.optionalWith(S.Number, { nullable: true }), + embedding: S.Union([S.Array(S.Number), S.String]), + index: S.optional(S.NullOr(S.Number)), }), ), model: S.String, - usage: S.optionalWith( - S.Struct({ + usage: S.optional(S.NullOr(S.Struct({ prompt_tokens: S.Number, total_tokens: S.Number, - cost: S.optionalWith(S.Number, { nullable: true }), - }), - { nullable: true }, - ), -}) {} + cost: S.optional(S.NullOr(S.Number)), + }))), +}) /** * Pricing information for the model @@ -2916,25 +2709,24 @@ export class CreateEmbeddings200 extends S.Struct({ export class PublicPricing extends S.Class("PublicPricing")({ prompt: BigNumberUnion, completion: BigNumberUnion, - request: S.optionalWith(BigNumberUnion, { nullable: true }), - image: S.optionalWith(BigNumberUnion, { nullable: true }), - image_token: S.optionalWith(BigNumberUnion, { nullable: true }), - image_output: S.optionalWith(BigNumberUnion, { nullable: true }), - audio: S.optionalWith(BigNumberUnion, { nullable: true }), - audio_output: S.optionalWith(BigNumberUnion, { nullable: true }), - input_audio_cache: S.optionalWith(BigNumberUnion, { nullable: true }), - web_search: S.optionalWith(BigNumberUnion, { nullable: true }), - internal_reasoning: S.optionalWith(BigNumberUnion, { nullable: true }), - input_cache_read: S.optionalWith(BigNumberUnion, { nullable: true }), - input_cache_write: S.optionalWith(BigNumberUnion, { nullable: true }), - discount: S.optionalWith(S.Number, { nullable: true }), + request: S.optional(S.NullOr(BigNumberUnion)), + image: S.optional(S.NullOr(BigNumberUnion)), + image_token: S.optional(S.NullOr(BigNumberUnion)), + image_output: S.optional(S.NullOr(BigNumberUnion)), + audio: S.optional(S.NullOr(BigNumberUnion)), + audio_output: S.optional(S.NullOr(BigNumberUnion)), + input_audio_cache: S.optional(S.NullOr(BigNumberUnion)), + web_search: S.optional(S.NullOr(BigNumberUnion)), + internal_reasoning: S.optional(S.NullOr(BigNumberUnion)), + input_cache_read: S.optional(S.NullOr(BigNumberUnion)), + input_cache_write: S.optional(S.NullOr(BigNumberUnion)), + discount: S.optional(S.NullOr(S.Number)), }) {} /** * Tokenizer type used by the model */ -export class ModelGroup extends S.Literal( - "Router", +export const ModelGroup = S.Literals(["Router", "Media", "Other", "GPT", @@ -2952,14 +2744,12 @@ export class ModelGroup extends S.Literal( "Llama4", "PaLM", "RWKV", - "Qwen3", -) {} + "Qwen3",]) /** * Instruction format type */ -export class ModelArchitectureInstructType extends S.Literal( - "none", +export const ModelArchitectureInstructType = S.Literals(["none", "airoboros", "alpaca", "alpaca-modif", @@ -2980,22 +2770,21 @@ export class ModelArchitectureInstructType extends S.Literal( "deepseek-r1", "deepseek-v3.1", "qwq", - "qwen3", -) {} + "qwen3",]) -export class InputModality extends S.Literal("text", "image", "file", "audio", "video") {} +export const InputModality = S.Literals(["text", "image", "file", "audio", "video"]) -export class OutputModality extends S.Literal("text", "image", "embeddings", "audio") {} +export const OutputModality = S.Literals(["text", "image", "embeddings", "audio"]) /** * Model architecture information */ export class ModelArchitecture extends S.Class("ModelArchitecture")({ - tokenizer: S.optionalWith(ModelGroup, { nullable: true }), + tokenizer: S.optional(S.NullOr(ModelGroup)), /** * Instruction format type */ - instruct_type: S.optionalWith(ModelArchitectureInstructType, { nullable: true }), + instruct_type: S.optional(S.NullOr(ModelArchitectureInstructType)), /** * Primary modality of the model */ @@ -3017,11 +2806,11 @@ export class TopProviderInfo extends S.Class("TopProviderInfo") /** * Context length from the top provider */ - context_length: S.optionalWith(S.Number, { nullable: true }), + context_length: S.optional(S.NullOr(S.Number)), /** * Maximum completion tokens from the top provider */ - max_completion_tokens: S.optionalWith(S.Number, { nullable: true }), + max_completion_tokens: S.optional(S.NullOr(S.Number)), /** * Whether the top provider moderates content */ @@ -3042,8 +2831,7 @@ export class PerRequestLimits extends S.Class("PerRequestLimit completion_tokens: S.Number, }) {} -export class Parameter extends S.Literal( - "temperature", +export const Parameter = S.Literals(["temperature", "top_p", "top_k", "min_p", @@ -3066,22 +2854,15 @@ export class Parameter extends S.Literal( "reasoning", "reasoning_effort", "web_search_options", - "verbosity", -) {} + "verbosity",]) /** * Default parameters for this model */ export class DefaultParameters extends S.Class("DefaultParameters")({ - temperature: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2)), { - nullable: true, - }), - top_p: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { - nullable: true, - }), - frequency_penalty: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { - nullable: true, - }), + temperature: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(2)))), + top_p: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(1)))), + frequency_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), }) {} /** @@ -3099,7 +2880,7 @@ export class Model extends S.Class("Model")({ /** * Hugging Face model identifier, if applicable */ - hugging_face_id: S.optionalWith(S.String, { nullable: true }), + hugging_face_id: S.optional(S.NullOr(S.String)), /** * Display name of the model */ @@ -3111,7 +2892,7 @@ export class Model extends S.Class("Model")({ /** * Description of the model */ - description: S.optionalWith(S.String, { nullable: true }), + description: S.optional(S.NullOr(S.String)), pricing: PublicPricing, /** * Maximum context length in tokens @@ -3128,13 +2909,13 @@ export class Model extends S.Class("Model")({ /** * The date after which the model may be removed. ISO 8601 date string (YYYY-MM-DD) or null if no expiration. */ - expiration_date: S.optionalWith(S.String, { nullable: true }), + expiration_date: S.optional(S.NullOr(S.String)), }) {} /** * List of available models */ -export class ModelsListResponseData extends S.Array(Model) {} +export const ModelsListResponseData = S.Array(Model) /** * List of available models @@ -3143,19 +2924,19 @@ export class ModelsListResponse extends S.Class("ModelsListR data: ModelsListResponseData, }) {} -export class GetGenerationParams extends S.Struct({ - id: S.String.pipe(S.minLength(1)), -}) {} +export const GetGenerationParams = S.Struct({ + id: S.String.check(S.isMinLength(1)), +}) /** * Type of API used for the generation */ -export class GetGeneration200DataApiType extends S.Literal("completions", "embeddings") {} +export const GetGeneration200DataApiType = S.Literals(["completions", "embeddings"]) /** * Generation response */ -export class GetGeneration200 extends S.Struct({ +export const GetGeneration200 = S.Struct({ /** * Generation data */ @@ -3293,7 +3074,7 @@ export class GetGeneration200 extends S.Struct({ */ router: S.NullOr(S.String), }), -}) {} +}) /** * Model count data @@ -3313,8 +3094,7 @@ export class ModelsCountResponse extends S.Class("ModelsCou /** * Filter models by use case category */ -export class GetModelsParamsCategory extends S.Literal( - "programming", +export const GetModelsParamsCategory = S.Literals(["programming", "roleplay", "marketing", "marketing/seo", @@ -3325,22 +3105,20 @@ export class GetModelsParamsCategory extends S.Literal( "finance", "health", "trivia", - "academia", -) {} + "academia",]) -export class GetModelsParams extends S.Struct({ +export const GetModelsParams = S.Struct({ /** * Filter models by use case category */ - category: S.optionalWith(GetModelsParamsCategory, { nullable: true }), - supported_parameters: S.optionalWith(S.String, { nullable: true }), -}) {} + category: S.optional(S.NullOr(GetModelsParamsCategory)), + supported_parameters: S.optional(S.NullOr(S.String)), +}) /** * Instruction format type */ -export class ListEndpointsResponseArchitectureEnumInstructType extends S.Literal( - "none", +export const ListEndpointsResponseArchitectureEnumInstructType = S.Literals(["none", "airoboros", "alpaca", "alpaca-modif", @@ -3361,19 +3139,18 @@ export class ListEndpointsResponseArchitectureEnumInstructType extends S.Literal "deepseek-r1", "deepseek-v3.1", "qwq", - "qwen3", -) {} + "qwen3",]) /** * Model architecture information */ -export class ListEndpointsResponseArchitecture extends S.Struct({ +export const ListEndpointsResponseArchitecture = S.Struct({ tokenizer: ModelGroup, /** * Instruction format type */ instruct_type: S.NullOr( - S.Literal( + S.Literals([ "none", "airoboros", "alpaca", @@ -3396,7 +3173,7 @@ export class ListEndpointsResponseArchitecture extends S.Struct({ "deepseek-v3.1", "qwq", "qwen3", - ), + ]), ), /** * Primary modality of the model @@ -3410,10 +3187,9 @@ export class ListEndpointsResponseArchitecture extends S.Struct({ * Supported output modalities */ output_modalities: S.Array(OutputModality), -}) {} +}) -export class PublicEndpointQuantizationEnum extends S.Literal( - "int4", +export const PublicEndpointQuantizationEnum = S.Literals(["int4", "int8", "fp4", "fp6", @@ -3421,12 +3197,11 @@ export class PublicEndpointQuantizationEnum extends S.Literal( "fp16", "bf16", "fp32", - "unknown", -) {} + "unknown",]) -export class PublicEndpointQuantization extends PublicEndpointQuantizationEnum {} +export const PublicEndpointQuantization = PublicEndpointQuantizationEnum -export class EndpointStatus extends S.Literal(0, -1, -2, -3, -5, -10) {} +export const EndpointStatus = S.Literals([0, -1, -2, -3, -5, -10]) /** * Latency percentiles in milliseconds over the last 30 minutes. Latency measures time to first token. Only visible when authenticated with an API key or cookie; returns null for unauthenticated requests. @@ -3453,7 +3228,7 @@ export class PercentileStats extends S.Class("PercentileStats") /** * Throughput percentiles in tokens per second over the last 30 minutes. Throughput measures output token generation speed. Only visible when authenticated with an API key or cookie; returns null for unauthenticated requests. */ -export class PublicEndpointThroughputLast30M extends S.Struct({ +export const PublicEndpointThroughputLast30M = S.Struct({ /** * Median (50th percentile) */ @@ -3470,7 +3245,7 @@ export class PublicEndpointThroughputLast30M extends S.Struct({ * 99th percentile */ p99: S.Number, -}) {} +}) /** * Information about a specific model endpoint @@ -3486,18 +3261,18 @@ export class PublicEndpoint extends S.Class("PublicEndpoint")({ pricing: S.Struct({ prompt: BigNumberUnion, completion: BigNumberUnion, - request: S.optionalWith(BigNumberUnion, { nullable: true }), - image: S.optionalWith(BigNumberUnion, { nullable: true }), - image_token: S.optionalWith(BigNumberUnion, { nullable: true }), - image_output: S.optionalWith(BigNumberUnion, { nullable: true }), - audio: S.optionalWith(BigNumberUnion, { nullable: true }), - audio_output: S.optionalWith(BigNumberUnion, { nullable: true }), - input_audio_cache: S.optionalWith(BigNumberUnion, { nullable: true }), - web_search: S.optionalWith(BigNumberUnion, { nullable: true }), - internal_reasoning: S.optionalWith(BigNumberUnion, { nullable: true }), - input_cache_read: S.optionalWith(BigNumberUnion, { nullable: true }), - input_cache_write: S.optionalWith(BigNumberUnion, { nullable: true }), - discount: S.optionalWith(S.Number, { nullable: true }), + request: S.optional(S.NullOr(BigNumberUnion)), + image: S.optional(S.NullOr(BigNumberUnion)), + image_token: S.optional(S.NullOr(BigNumberUnion)), + image_output: S.optional(S.NullOr(BigNumberUnion)), + audio: S.optional(S.NullOr(BigNumberUnion)), + audio_output: S.optional(S.NullOr(BigNumberUnion)), + input_audio_cache: S.optional(S.NullOr(BigNumberUnion)), + web_search: S.optional(S.NullOr(BigNumberUnion)), + internal_reasoning: S.optional(S.NullOr(BigNumberUnion)), + input_cache_read: S.optional(S.NullOr(BigNumberUnion)), + input_cache_write: S.optional(S.NullOr(BigNumberUnion)), + discount: S.optional(S.NullOr(S.Number)), }), provider_name: ProviderName, tag: S.String, @@ -3505,7 +3280,7 @@ export class PublicEndpoint extends S.Class("PublicEndpoint")({ max_completion_tokens: S.NullOr(S.Number), max_prompt_tokens: S.NullOr(S.Number), supported_parameters: S.Array(Parameter), - status: S.optionalWith(EndpointStatus, { nullable: true }), + status: S.optional(S.NullOr(EndpointStatus)), uptime_last_30m: S.NullOr(S.Number), supports_implicit_caching: S.Boolean, latency_last_30m: S.NullOr(PercentileStats), @@ -3539,15 +3314,15 @@ export class ListEndpointsResponse extends S.Class("ListE endpoints: S.Array(PublicEndpoint), }) {} -export class ListEndpoints200 extends S.Struct({ +export const ListEndpoints200 = S.Struct({ data: ListEndpointsResponse, -}) {} +}) -export class ListEndpointsZdr200 extends S.Struct({ +export const ListEndpointsZdr200 = S.Struct({ data: S.Array(PublicEndpoint), -}) {} +}) -export class ListProviders200 extends S.Struct({ +export const ListProviders200 = S.Struct({ data: S.Array( S.Struct({ /** @@ -3565,27 +3340,27 @@ export class ListProviders200 extends S.Struct({ /** * URL to the provider's terms of service */ - terms_of_service_url: S.optionalWith(S.String, { nullable: true }), + terms_of_service_url: S.optional(S.NullOr(S.String)), /** * URL to the provider's status page */ - status_page_url: S.optionalWith(S.String, { nullable: true }), + status_page_url: S.optional(S.NullOr(S.String)), }), ), -}) {} +}) -export class ListParams extends S.Struct({ +export const ListParams = S.Struct({ /** * Whether to include disabled API keys in the response */ - include_disabled: S.optionalWith(S.String, { nullable: true }), + include_disabled: S.optional(S.NullOr(S.String)), /** * Number of API keys to skip for pagination */ - offset: S.optionalWith(S.String, { nullable: true }), -}) {} + offset: S.optional(S.NullOr(S.String)), +}) -export class List200 extends S.Struct({ +export const List200 = S.Struct({ /** * List of API keys */ @@ -3666,40 +3441,40 @@ export class List200 extends S.Struct({ /** * ISO 8601 UTC timestamp when the API key expires, or null if no expiration */ - expires_at: S.optionalWith(S.String, { nullable: true }), + expires_at: S.optional(S.NullOr(S.String)), }), ), -}) {} +}) /** * Type of limit reset for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. */ -export class CreateKeysRequestLimitReset extends S.Literal("daily", "weekly", "monthly") {} +export const CreateKeysRequestLimitReset = S.Literals(["daily", "weekly", "monthly"]) export class CreateKeysRequest extends S.Class("CreateKeysRequest")({ /** * Name for the new API key */ - name: S.String.pipe(S.minLength(1)), + name: S.String.check(S.isMinLength(1)), /** * Optional spending limit for the API key in USD */ - limit: S.optionalWith(S.Number, { nullable: true }), + limit: S.optional(S.NullOr(S.Number)), /** * Type of limit reset for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. */ - limit_reset: S.optionalWith(CreateKeysRequestLimitReset, { nullable: true }), + limit_reset: S.optional(S.NullOr(CreateKeysRequestLimitReset)), /** * Whether to include BYOK usage in the limit */ - include_byok_in_limit: S.optionalWith(S.Boolean, { nullable: true }), + include_byok_in_limit: S.optional(S.NullOr(S.Boolean)), /** * Optional ISO 8601 UTC timestamp when the API key should expire. Must be UTC, other timezones will be rejected */ - expires_at: S.optionalWith(S.String, { nullable: true }), + expires_at: S.optional(S.NullOr(S.String)), }) {} -export class CreateKeys201 extends S.Struct({ +export const CreateKeys201 = S.Struct({ /** * The created API key information */ @@ -3779,15 +3554,15 @@ export class CreateKeys201 extends S.Struct({ /** * ISO 8601 UTC timestamp when the API key expires, or null if no expiration */ - expires_at: S.optionalWith(S.String, { nullable: true }), + expires_at: S.optional(S.NullOr(S.String)), }), /** * The actual API key string (only shown once) */ key: S.String, -}) {} +}) -export class GetKey200 extends S.Struct({ +export const GetKey200 = S.Struct({ /** * The API key information */ @@ -3867,46 +3642,46 @@ export class GetKey200 extends S.Struct({ /** * ISO 8601 UTC timestamp when the API key expires, or null if no expiration */ - expires_at: S.optionalWith(S.String, { nullable: true }), + expires_at: S.optional(S.NullOr(S.String)), }), -}) {} +}) -export class DeleteKeys200 extends S.Struct({ +export const DeleteKeys200 = S.Struct({ /** * Confirmation that the API key was deleted */ deleted: S.Literal(true), -}) {} +}) /** * New limit reset type for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. */ -export class UpdateKeysRequestLimitReset extends S.Literal("daily", "weekly", "monthly") {} +export const UpdateKeysRequestLimitReset = S.Literals(["daily", "weekly", "monthly"]) export class UpdateKeysRequest extends S.Class("UpdateKeysRequest")({ /** * New name for the API key */ - name: S.optionalWith(S.String, { nullable: true }), + name: S.optional(S.NullOr(S.String)), /** * Whether to disable the API key */ - disabled: S.optionalWith(S.Boolean, { nullable: true }), + disabled: S.optional(S.NullOr(S.Boolean)), /** * New spending limit for the API key in USD */ - limit: S.optionalWith(S.Number, { nullable: true }), + limit: S.optional(S.NullOr(S.Number)), /** * New limit reset type for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. */ - limit_reset: S.optionalWith(UpdateKeysRequestLimitReset, { nullable: true }), + limit_reset: S.optional(S.NullOr(UpdateKeysRequestLimitReset)), /** * Whether to include BYOK usage in the limit */ - include_byok_in_limit: S.optionalWith(S.Boolean, { nullable: true }), + include_byok_in_limit: S.optional(S.NullOr(S.Boolean)), }) {} -export class UpdateKeys200 extends S.Struct({ +export const UpdateKeys200 = S.Struct({ /** * The updated API key information */ @@ -3986,22 +3761,22 @@ export class UpdateKeys200 extends S.Struct({ /** * ISO 8601 UTC timestamp when the API key expires, or null if no expiration */ - expires_at: S.optionalWith(S.String, { nullable: true }), + expires_at: S.optional(S.NullOr(S.String)), }), -}) {} +}) -export class ListGuardrailsParams extends S.Struct({ +export const ListGuardrailsParams = S.Struct({ /** * Number of records to skip for pagination */ - offset: S.optionalWith(S.String, { nullable: true }), + offset: S.optional(S.NullOr(S.String)), /** * Maximum number of records to return (max 100) */ - limit: S.optionalWith(S.String, { nullable: true }), -}) {} + limit: S.optional(S.NullOr(S.String)), +}) -export class ListGuardrails200 extends S.Struct({ +export const ListGuardrails200 = S.Struct({ /** * List of guardrails */ @@ -4018,27 +3793,27 @@ export class ListGuardrails200 extends S.Struct({ /** * Description of the guardrail */ - description: S.optionalWith(S.String, { nullable: true }), + description: S.optional(S.NullOr(S.String)), /** * Spending limit in USD */ - limit_usd: S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + limit_usd: S.optional(S.NullOr(S.Number.check(S.isGreaterThan(0)))), /** * Interval at which the limit resets (daily, weekly, monthly) */ - reset_interval: S.optionalWith(S.Literal("daily", "weekly", "monthly"), { nullable: true }), + reset_interval: S.optional(S.NullOr(S.Literals(["daily", "weekly", "monthly"]))), /** * List of allowed provider IDs */ - allowed_providers: S.optionalWith(S.Array(S.String), { nullable: true }), + allowed_providers: S.optional(S.NullOr(S.Array(S.String))), /** * Array of model canonical_slugs (immutable identifiers) */ - allowed_models: S.optionalWith(S.Array(S.String), { nullable: true }), + allowed_models: S.optional(S.NullOr(S.Array(S.String))), /** * Whether to enforce zero data retention */ - enforce_zdr: S.optionalWith(S.Boolean, { nullable: true }), + enforce_zdr: S.optional(S.NullOr(S.Boolean)), /** * ISO 8601 timestamp of when the guardrail was created */ @@ -4046,57 +3821,57 @@ export class ListGuardrails200 extends S.Struct({ /** * ISO 8601 timestamp of when the guardrail was last updated */ - updated_at: S.optionalWith(S.String, { nullable: true }), + updated_at: S.optional(S.NullOr(S.String)), }), ), /** * Total number of guardrails */ total_count: S.Number, -}) {} +}) /** * Interval at which the limit resets (daily, weekly, monthly) */ -export class CreateGuardrailRequestResetInterval extends S.Literal("daily", "weekly", "monthly") {} +export const CreateGuardrailRequestResetInterval = S.Literals(["daily", "weekly", "monthly"]) export class CreateGuardrailRequest extends S.Class("CreateGuardrailRequest")({ /** * Name for the new guardrail */ - name: S.String.pipe(S.minLength(1), S.maxLength(200)), + name: S.String.check(S.isMinLength(1), S.isMaxLength(200)), /** * Description of the guardrail */ - description: S.optionalWith(S.String.pipe(S.maxLength(1000)), { nullable: true }), + description: S.optional(S.NullOr(S.String.check(S.isMaxLength(1000)))), /** * Spending limit in USD */ - limit_usd: S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + limit_usd: S.optional(S.NullOr(S.Number.check(S.isGreaterThan(0)))), /** * Interval at which the limit resets (daily, weekly, monthly) */ - reset_interval: S.optionalWith(CreateGuardrailRequestResetInterval, { nullable: true }), + reset_interval: S.optional(S.NullOr(CreateGuardrailRequestResetInterval)), /** * List of allowed provider IDs */ - allowed_providers: S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1)), { nullable: true }), + allowed_providers: S.optional(S.NullOr(S.NonEmptyArray(S.String).check(S.isMinLength(1)))), /** * Array of model identifiers (slug or canonical_slug accepted) */ - allowed_models: S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1)), { nullable: true }), + allowed_models: S.optional(S.NullOr(S.NonEmptyArray(S.String).check(S.isMinLength(1)))), /** * Whether to enforce zero data retention */ - enforce_zdr: S.optionalWith(S.Boolean, { nullable: true }), + enforce_zdr: S.optional(S.NullOr(S.Boolean)), }) {} /** * Interval at which the limit resets (daily, weekly, monthly) */ -export class CreateGuardrail201DataResetInterval extends S.Literal("daily", "weekly", "monthly") {} +export const CreateGuardrail201DataResetInterval = S.Literals(["daily", "weekly", "monthly"]) -export class CreateGuardrail201 extends S.Struct({ +export const CreateGuardrail201 = S.Struct({ /** * The created guardrail */ @@ -4112,27 +3887,27 @@ export class CreateGuardrail201 extends S.Struct({ /** * Description of the guardrail */ - description: S.optionalWith(S.String, { nullable: true }), + description: S.optional(S.NullOr(S.String)), /** * Spending limit in USD */ - limit_usd: S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + limit_usd: S.optional(S.NullOr(S.Number.check(S.isGreaterThan(0)))), /** * Interval at which the limit resets (daily, weekly, monthly) */ - reset_interval: S.optionalWith(CreateGuardrail201DataResetInterval, { nullable: true }), + reset_interval: S.optional(S.NullOr(CreateGuardrail201DataResetInterval)), /** * List of allowed provider IDs */ - allowed_providers: S.optionalWith(S.Array(S.String), { nullable: true }), + allowed_providers: S.optional(S.NullOr(S.Array(S.String))), /** * Array of model canonical_slugs (immutable identifiers) */ - allowed_models: S.optionalWith(S.Array(S.String), { nullable: true }), + allowed_models: S.optional(S.NullOr(S.Array(S.String))), /** * Whether to enforce zero data retention */ - enforce_zdr: S.optionalWith(S.Boolean, { nullable: true }), + enforce_zdr: S.optional(S.NullOr(S.Boolean)), /** * ISO 8601 timestamp of when the guardrail was created */ @@ -4140,16 +3915,16 @@ export class CreateGuardrail201 extends S.Struct({ /** * ISO 8601 timestamp of when the guardrail was last updated */ - updated_at: S.optionalWith(S.String, { nullable: true }), + updated_at: S.optional(S.NullOr(S.String)), }), -}) {} +}) /** * Interval at which the limit resets (daily, weekly, monthly) */ -export class GetGuardrail200DataResetInterval extends S.Literal("daily", "weekly", "monthly") {} +export const GetGuardrail200DataResetInterval = S.Literals(["daily", "weekly", "monthly"]) -export class GetGuardrail200 extends S.Struct({ +export const GetGuardrail200 = S.Struct({ /** * The guardrail */ @@ -4165,27 +3940,27 @@ export class GetGuardrail200 extends S.Struct({ /** * Description of the guardrail */ - description: S.optionalWith(S.String, { nullable: true }), + description: S.optional(S.NullOr(S.String)), /** * Spending limit in USD */ - limit_usd: S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + limit_usd: S.optional(S.NullOr(S.Number.check(S.isGreaterThan(0)))), /** * Interval at which the limit resets (daily, weekly, monthly) */ - reset_interval: S.optionalWith(GetGuardrail200DataResetInterval, { nullable: true }), + reset_interval: S.optional(S.NullOr(GetGuardrail200DataResetInterval)), /** * List of allowed provider IDs */ - allowed_providers: S.optionalWith(S.Array(S.String), { nullable: true }), + allowed_providers: S.optional(S.NullOr(S.Array(S.String))), /** * Array of model canonical_slugs (immutable identifiers) */ - allowed_models: S.optionalWith(S.Array(S.String), { nullable: true }), + allowed_models: S.optional(S.NullOr(S.Array(S.String))), /** * Whether to enforce zero data retention */ - enforce_zdr: S.optionalWith(S.Boolean, { nullable: true }), + enforce_zdr: S.optional(S.NullOr(S.Boolean)), /** * ISO 8601 timestamp of when the guardrail was created */ @@ -4193,59 +3968,59 @@ export class GetGuardrail200 extends S.Struct({ /** * ISO 8601 timestamp of when the guardrail was last updated */ - updated_at: S.optionalWith(S.String, { nullable: true }), + updated_at: S.optional(S.NullOr(S.String)), }), -}) {} +}) -export class DeleteGuardrail200 extends S.Struct({ +export const DeleteGuardrail200 = S.Struct({ /** * Confirmation that the guardrail was deleted */ deleted: S.Literal(true), -}) {} +}) /** * Interval at which the limit resets (daily, weekly, monthly) */ -export class UpdateGuardrailRequestResetInterval extends S.Literal("daily", "weekly", "monthly") {} +export const UpdateGuardrailRequestResetInterval = S.Literals(["daily", "weekly", "monthly"]) export class UpdateGuardrailRequest extends S.Class("UpdateGuardrailRequest")({ /** * New name for the guardrail */ - name: S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(200)), { nullable: true }), + name: S.optional(S.NullOr(S.String.check(S.isMinLength(1), S.isMaxLength(200)))), /** * New description for the guardrail */ - description: S.optionalWith(S.String.pipe(S.maxLength(1000)), { nullable: true }), + description: S.optional(S.NullOr(S.String.check(S.isMaxLength(1000)))), /** * New spending limit in USD */ - limit_usd: S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + limit_usd: S.optional(S.NullOr(S.Number.check(S.isGreaterThan(0)))), /** * Interval at which the limit resets (daily, weekly, monthly) */ - reset_interval: S.optionalWith(UpdateGuardrailRequestResetInterval, { nullable: true }), + reset_interval: S.optional(S.NullOr(UpdateGuardrailRequestResetInterval)), /** * New list of allowed provider IDs */ - allowed_providers: S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1)), { nullable: true }), + allowed_providers: S.optional(S.NullOr(S.NonEmptyArray(S.String).check(S.isMinLength(1)))), /** * Array of model identifiers (slug or canonical_slug accepted) */ - allowed_models: S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1)), { nullable: true }), + allowed_models: S.optional(S.NullOr(S.NonEmptyArray(S.String).check(S.isMinLength(1)))), /** * Whether to enforce zero data retention */ - enforce_zdr: S.optionalWith(S.Boolean, { nullable: true }), + enforce_zdr: S.optional(S.NullOr(S.Boolean)), }) {} /** * Interval at which the limit resets (daily, weekly, monthly) */ -export class UpdateGuardrail200DataResetInterval extends S.Literal("daily", "weekly", "monthly") {} +export const UpdateGuardrail200DataResetInterval = S.Literals(["daily", "weekly", "monthly"]) -export class UpdateGuardrail200 extends S.Struct({ +export const UpdateGuardrail200 = S.Struct({ /** * The updated guardrail */ @@ -4261,27 +4036,27 @@ export class UpdateGuardrail200 extends S.Struct({ /** * Description of the guardrail */ - description: S.optionalWith(S.String, { nullable: true }), + description: S.optional(S.NullOr(S.String)), /** * Spending limit in USD */ - limit_usd: S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + limit_usd: S.optional(S.NullOr(S.Number.check(S.isGreaterThan(0)))), /** * Interval at which the limit resets (daily, weekly, monthly) */ - reset_interval: S.optionalWith(UpdateGuardrail200DataResetInterval, { nullable: true }), + reset_interval: S.optional(S.NullOr(UpdateGuardrail200DataResetInterval)), /** * List of allowed provider IDs */ - allowed_providers: S.optionalWith(S.Array(S.String), { nullable: true }), + allowed_providers: S.optional(S.NullOr(S.Array(S.String))), /** * Array of model canonical_slugs (immutable identifiers) */ - allowed_models: S.optionalWith(S.Array(S.String), { nullable: true }), + allowed_models: S.optional(S.NullOr(S.Array(S.String))), /** * Whether to enforce zero data retention */ - enforce_zdr: S.optionalWith(S.Boolean, { nullable: true }), + enforce_zdr: S.optional(S.NullOr(S.Boolean)), /** * ISO 8601 timestamp of when the guardrail was created */ @@ -4289,22 +4064,22 @@ export class UpdateGuardrail200 extends S.Struct({ /** * ISO 8601 timestamp of when the guardrail was last updated */ - updated_at: S.optionalWith(S.String, { nullable: true }), + updated_at: S.optional(S.NullOr(S.String)), }), -}) {} +}) -export class ListKeyAssignmentsParams extends S.Struct({ +export const ListKeyAssignmentsParams = S.Struct({ /** * Number of records to skip for pagination */ - offset: S.optionalWith(S.String, { nullable: true }), + offset: S.optional(S.NullOr(S.String)), /** * Maximum number of records to return (max 100) */ - limit: S.optionalWith(S.String, { nullable: true }), -}) {} + limit: S.optional(S.NullOr(S.String)), +}) -export class ListKeyAssignments200 extends S.Struct({ +export const ListKeyAssignments200 = S.Struct({ /** * List of key assignments */ @@ -4344,20 +4119,20 @@ export class ListKeyAssignments200 extends S.Struct({ * Total number of key assignments for this guardrail */ total_count: S.Number, -}) {} +}) -export class ListMemberAssignmentsParams extends S.Struct({ +export const ListMemberAssignmentsParams = S.Struct({ /** * Number of records to skip for pagination */ - offset: S.optionalWith(S.String, { nullable: true }), + offset: S.optional(S.NullOr(S.String)), /** * Maximum number of records to return (max 100) */ - limit: S.optionalWith(S.String, { nullable: true }), -}) {} + limit: S.optional(S.NullOr(S.String)), +}) -export class ListMemberAssignments200 extends S.Struct({ +export const ListMemberAssignments200 = S.Struct({ /** * List of member assignments */ @@ -4393,20 +4168,20 @@ export class ListMemberAssignments200 extends S.Struct({ * Total number of member assignments */ total_count: S.Number, -}) {} +}) -export class ListGuardrailKeyAssignmentsParams extends S.Struct({ +export const ListGuardrailKeyAssignmentsParams = S.Struct({ /** * Number of records to skip for pagination */ - offset: S.optionalWith(S.String, { nullable: true }), + offset: S.optional(S.NullOr(S.String)), /** * Maximum number of records to return (max 100) */ - limit: S.optionalWith(S.String, { nullable: true }), -}) {} + limit: S.optional(S.NullOr(S.String)), +}) -export class ListGuardrailKeyAssignments200 extends S.Struct({ +export const ListGuardrailKeyAssignments200 = S.Struct({ /** * List of key assignments */ @@ -4446,7 +4221,7 @@ export class ListGuardrailKeyAssignments200 extends S.Struct({ * Total number of key assignments for this guardrail */ total_count: S.Number, -}) {} +}) export class BulkAssignKeysToGuardrailRequest extends S.Class( "BulkAssignKeysToGuardrailRequest", @@ -4454,28 +4229,28 @@ export class BulkAssignKeysToGuardrailRequest extends S.Class( "BulkAssignMembersToGuardrailRequest", @@ -4519,15 +4294,15 @@ export class BulkAssignMembersToGuardrailRequest extends S.Class( "BulkUnassignKeysFromGuardrailRequest", @@ -4535,15 +4310,15 @@ export class BulkUnassignKeysFromGuardrailRequest extends S.Class( "BulkUnassignMembersFromGuardrailRequest", @@ -4551,17 +4326,17 @@ export class BulkUnassignMembersFromGuardrailRequest extends S.Class( "ExchangeAuthCodeForAPIKeyRequest", @@ -4665,16 +4440,14 @@ export class ExchangeAuthCodeForAPIKeyRequest extends S.Class( "CreateAuthKeysCodeRequest", @@ -4700,22 +4473,22 @@ export class CreateAuthKeysCodeRequest extends S.Class( "ChatMessageContentItemCacheControl", )({ type: S.Literal("ephemeral"), - ttl: S.optionalWith(ChatMessageContentItemCacheControlTtl, { nullable: true }), + ttl: S.optional(S.NullOr(ChatMessageContentItemCacheControlTtl)), }) {} export class ChatMessageContentItemText extends S.Class( @@ -4835,16 +4608,16 @@ export class ChatMessageContentItemText extends S.Class("SystemMessage")({ role: S.Literal("system"), - content: S.Union(S.String, S.Array(ChatMessageContentItemText)), - name: S.optionalWith(S.String, { nullable: true }), + content: S.Union([S.String, S.Array(ChatMessageContentItemText)]), + name: S.optional(S.NullOr(S.String)), }) {} -export class ChatMessageContentItemImageImageUrlDetail extends S.Literal("auto", "low", "high") {} +export const ChatMessageContentItemImageImageUrlDetail = S.Literals(["auto", "low", "high"]) export class ChatMessageContentItemImage extends S.Class( "ChatMessageContentItemImage", @@ -4852,7 +4625,7 @@ export class ChatMessageContentItemImage extends S.Class("UserMessage")({ role: S.Literal("user"), - content: S.Union(S.String, S.Array(ChatMessageContentItem)), - name: S.optionalWith(S.String, { nullable: true }), + content: S.Union([S.String, S.Array(ChatMessageContentItem)]), + name: S.optional(S.NullOr(S.String)), }) {} export class ChatMessageToolCall extends S.Class("ChatMessageToolCall")({ @@ -4885,68 +4658,61 @@ export class ChatMessageToolCall extends S.Class("ChatMessa }), }) {} -export class Schema3 extends S.Union(S.String, S.Null) {} +export const Schema3 = S.Union([S.String, S.Null]) -export class Schema4Enum extends S.Literal( - "unknown", +export const Schema4Enum = S.Literals(["unknown", "openai-responses-v1", "azure-openai-responses-v1", "xai-responses-v1", "anthropic-claude-v1", - "google-gemini-v1", -) {} + "google-gemini-v1",]) -export class Schema4 extends S.Union(Schema4Enum, S.Null) {} +export const Schema4 = S.Union([Schema4Enum, S.Null]) -export class Schema5 extends S.Number {} +export const Schema5 = S.Number -export class Schema2 extends S.Record({ key: S.String, value: S.Unknown }) {} +export const Schema2 = S.Record(S.String, S.Unknown) export class AssistantMessage extends S.Class("AssistantMessage")({ role: S.Literal("assistant"), - content: S.optionalWith(S.Union(S.String, S.Array(ChatMessageContentItem)), { nullable: true }), - name: S.optionalWith(S.String, { nullable: true }), - tool_calls: S.optionalWith(S.Array(ChatMessageToolCall), { nullable: true }), - refusal: S.optionalWith(S.String, { nullable: true }), - reasoning: S.optionalWith(S.String, { nullable: true }), - reasoning_details: S.optionalWith(S.Array(ReasoningDetail), { nullable: true }), - images: S.optionalWith( - S.Array( + content: S.optional(S.NullOr(S.Union([S.String, S.Array(ChatMessageContentItem)]))), + name: S.optional(S.NullOr(S.String)), + tool_calls: S.optional(S.NullOr(S.Array(ChatMessageToolCall))), + refusal: S.optional(S.NullOr(S.String)), + reasoning: S.optional(S.NullOr(S.String)), + reasoning_details: S.optional(S.NullOr(S.Array(ReasoningDetail))), + images: S.optional(S.NullOr(S.Array( S.Struct({ image_url: S.Struct({ url: S.String, }), }), - ), - { nullable: true }, - ), - annotations: S.optionalWith(S.Array(AnnotationDetail), { nullable: true }), + ))), + annotations: S.optional(S.NullOr(S.Array(AnnotationDetail))), }) {} export class ToolResponseMessage extends S.Class("ToolResponseMessage")({ role: S.Literal("tool"), - content: S.Union(S.String, S.Array(ChatMessageContentItem)), + content: S.Union([S.String, S.Array(ChatMessageContentItem)]), tool_call_id: S.String, }) {} -export class Message extends S.Record({ key: S.String, value: S.Unknown }) {} +export const Message = S.Record(S.String, S.Unknown) -export class ModelName extends S.String {} +export const ModelName = S.String -export class ChatGenerationParamsReasoningEffortEnum extends S.Literal( - "xhigh", +export const ChatGenerationParamsReasoningEffortEnum = S.Literals(["xhigh", "high", "medium", "low", "minimal", - "none", -) {} + "none",]) export class JSONSchemaConfig extends S.Class("JSONSchemaConfig")({ - name: S.String.pipe(S.maxLength(64)), - description: S.optionalWith(S.String, { nullable: true }), - schema: S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), - strict: S.optionalWith(S.Boolean, { nullable: true }), + name: S.String.check(S.isMaxLength(64)), + description: S.optional(S.NullOr(S.String)), + schema: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), + strict: S.optional(S.NullOr(S.Boolean)), }) {} export class ResponseFormatJSONSchema extends S.Class("ResponseFormatJSONSchema")({ @@ -4962,7 +4728,7 @@ export class ResponseFormatTextGrammar extends S.Class("ChatStreamOptions")({ - include_usage: S.optionalWith(S.Boolean, { nullable: true }), + include_usage: S.optional(S.NullOr(S.Boolean)), }) {} export class NamedToolChoice extends S.Class("NamedToolChoice")({ @@ -4972,20 +4738,18 @@ export class NamedToolChoice extends S.Class("NamedToolChoice") }), }) {} -export class ToolChoiceOption extends S.Union( - S.Literal("none"), +export const ToolChoiceOption = S.Union([S.Literal("none"), S.Literal("auto"), S.Literal("required"), - NamedToolChoice, -) {} + NamedToolChoice,]) export class ToolDefinitionJson extends S.Class("ToolDefinitionJson")({ type: S.Literal("function"), function: S.Struct({ - name: S.String.pipe(S.maxLength(64)), - description: S.optionalWith(S.String, { nullable: true }), - parameters: S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), - strict: S.optionalWith(S.Boolean, { nullable: true }), + name: S.String.check(S.isMaxLength(64)), + description: S.optional(S.NullOr(S.String)), + parameters: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), + strict: S.optional(S.NullOr(S.Boolean)), }), }) {} @@ -4993,169 +4757,129 @@ export class ChatGenerationParams extends S.Class("ChatGen /** * When multiple model providers are available, optionally indicate your routing preference. */ - provider: S.optionalWith( - S.Struct({ + provider: S.optional(S.NullOr(S.Struct({ /** * Whether to allow backup providers to serve requests * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. */ - allow_fallbacks: S.optionalWith(S.Boolean, { nullable: true }), + allow_fallbacks: S.optional(S.NullOr(S.Boolean)), /** * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. */ - require_parameters: S.optionalWith(S.Boolean, { nullable: true }), + require_parameters: S.optional(S.NullOr(S.Boolean)), /** * Data collection setting. If no available model provider meets the requirement, your request will return an error. * - allow: (default) allow providers which store user data non-transiently and may train on it * * - deny: use only providers which do not collect user data. */ - data_collection: S.optionalWith(S.Literal("deny", "allow"), { nullable: true }), - zdr: S.optionalWith(S.Boolean, { nullable: true }), - enforce_distillable_text: S.optionalWith(S.Boolean, { nullable: true }), + data_collection: S.optional(S.NullOr(S.Literals(["deny", "allow"]))), + zdr: S.optional(S.NullOr(S.Boolean)), + enforce_distillable_text: S.optional(S.NullOr(S.Boolean)), /** * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. */ - order: S.optionalWith(Schema0, { nullable: true }), + order: S.optional(S.NullOr(Schema0)), /** * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. */ - only: S.optionalWith(Schema0, { nullable: true }), + only: S.optional(S.NullOr(Schema0)), /** * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. */ - ignore: S.optionalWith(Schema0, { nullable: true }), + ignore: S.optional(S.NullOr(Schema0)), /** * A list of quantization levels to filter the provider by. */ - quantizations: S.optionalWith( - S.Array(S.Literal("int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown")), - { nullable: true }, - ), + quantizations: S.optional(S.Array(S.Literals(["int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown"]))), /** * The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. */ - sort: S.optionalWith(ProviderSortUnion, { nullable: true }), + sort: S.optional(S.NullOr(ProviderSortUnion)), /** * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. */ - max_price: S.optionalWith( - S.Struct({ - prompt: S.optionalWith(Schema1, { nullable: true }), - completion: S.optionalWith(Schema1, { nullable: true }), - image: S.optionalWith(Schema1, { nullable: true }), - audio: S.optionalWith(Schema1, { nullable: true }), - request: S.optionalWith(Schema1, { nullable: true }), - }), - { nullable: true }, - ), + max_price: S.optional(S.Struct({ + prompt: S.optional(S.NullOr(Schema1)), + completion: S.optional(S.NullOr(Schema1)), + image: S.optional(S.NullOr(Schema1)), + audio: S.optional(S.NullOr(Schema1)), + request: S.optional(S.NullOr(Schema1)), + })), /** * Preferred minimum throughput (in tokens per second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints below the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. */ - preferred_min_throughput: S.optionalWith( - S.Union( + preferred_min_throughput: S.optional(S.Union([ S.Number, S.Struct({ - p50: S.optionalWith(S.Number, { nullable: true }), - p75: S.optionalWith(S.Number, { nullable: true }), - p90: S.optionalWith(S.Number, { nullable: true }), - p99: S.optionalWith(S.Number, { nullable: true }), + p50: S.optional(S.NullOr(S.Number)), + p75: S.optional(S.NullOr(S.Number)), + p90: S.optional(S.NullOr(S.Number)), + p99: S.optional(S.NullOr(S.Number)), }), - ), - { nullable: true }, - ), + ])), /** * Preferred maximum latency (in seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints above the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. */ - preferred_max_latency: S.optionalWith( - S.Union( + preferred_max_latency: S.optional(S.Union([ S.Number, S.Struct({ - p50: S.optionalWith(S.Number, { nullable: true }), - p75: S.optionalWith(S.Number, { nullable: true }), - p90: S.optionalWith(S.Number, { nullable: true }), - p99: S.optionalWith(S.Number, { nullable: true }), + p50: S.optional(S.NullOr(S.Number)), + p75: S.optional(S.NullOr(S.Number)), + p90: S.optional(S.NullOr(S.Number)), + p99: S.optional(S.NullOr(S.Number)), }), - ), - { nullable: true }, - ), - }), - { nullable: true }, - ), + ])), + }))), /** * Plugins you want to enable for this request, including their settings. */ - plugins: S.optionalWith(S.Array(S.Record({ key: S.String, value: S.Unknown })), { nullable: true }), - route: S.optionalWith(ChatGenerationParamsRouteEnum, { nullable: true }), - user: S.optionalWith(S.String, { nullable: true }), + plugins: S.optional(S.NullOr(S.Array(S.Record(S.String, S.Unknown)))), + route: S.optional(S.NullOr(ChatGenerationParamsRouteEnum)), + user: S.optional(S.NullOr(S.String)), /** * A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters. */ - session_id: S.optionalWith(S.String.pipe(S.maxLength(128)), { nullable: true }), - messages: S.NonEmptyArray(Message).pipe(S.minItems(1)), - model: S.optionalWith(ModelName, { nullable: true }), - models: S.optionalWith(S.Array(ModelName), { nullable: true }), - frequency_penalty: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { - nullable: true, - }), - logit_bias: S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), - logprobs: S.optionalWith(S.Boolean, { nullable: true }), - top_logprobs: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(20)), { - nullable: true, - }), - max_completion_tokens: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }), - max_tokens: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(1)), { nullable: true }), - metadata: S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), - presence_penalty: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { - nullable: true, - }), - reasoning: S.optionalWith( - S.Struct({ - effort: S.optionalWith(ChatGenerationParamsReasoningEffortEnum, { nullable: true }), - summary: S.optionalWith(ReasoningSummaryVerbosity, { nullable: true }), - }), - { nullable: true }, - ), - response_format: S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), - seed: S.optionalWith( - S.Int.pipe(S.greaterThanOrEqualTo(-9007199254740991), S.lessThanOrEqualTo(9007199254740991)), - { - nullable: true, - }, - ), - stop: S.optionalWith(S.Union(S.String, S.Array(S.String).pipe(S.maxItems(4))), { nullable: true }), - stream: S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), - stream_options: S.optionalWith(ChatStreamOptions, { nullable: true }), - temperature: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2)), { - nullable: true, - default: () => 1 as const, - }), - tool_choice: S.optionalWith(ToolChoiceOption, { nullable: true }), - tools: S.optionalWith(S.Array(ToolDefinitionJson), { nullable: true }), - top_p: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { - nullable: true, - default: () => 1 as const, - }), - debug: S.optionalWith( - S.Struct({ - echo_upstream_body: S.optionalWith(S.Boolean, { nullable: true }), - }), - { nullable: true }, - ), - image_config: S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), - modalities: S.optionalWith(S.Array(S.Literal("text", "image")), { nullable: true }), -}) {} - -export class ChatCompletionFinishReason extends S.Literal( - "tool_calls", + session_id: S.optional(S.NullOr(S.String.check(S.isMaxLength(128)))), + messages: S.NonEmptyArray(Message).check(S.isMinLength(1)), + model: S.optional(S.NullOr(ModelName)), + models: S.optional(S.NullOr(S.Array(ModelName))), + frequency_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), + logit_bias: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), + logprobs: S.optional(S.NullOr(S.Boolean)), + top_logprobs: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(20)))), + max_completion_tokens: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(1)))), + max_tokens: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(1)))), + metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), + presence_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), + reasoning: S.optional(S.NullOr(S.Struct({ + effort: S.optional(S.NullOr(ChatGenerationParamsReasoningEffortEnum)), + summary: S.optional(S.NullOr(ReasoningSummaryVerbosity)), + }))), + response_format: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), + seed: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(-9007199254740991), S.isLessThanOrEqualTo(9007199254740991)))), + stop: S.optional(S.NullOr(S.Union([S.String, S.Array(S.String).check(S.isMaxLength(4))]))), + stream: S.NullOr(S.Boolean).pipe(S.optional, S.withDecodingDefault(() => false as const)), + stream_options: S.optional(S.NullOr(ChatStreamOptions)), + temperature: S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(2))).pipe(S.optional, S.withDecodingDefault(() => 1 as const)), + tool_choice: S.optional(S.NullOr(ToolChoiceOption)), + tools: S.optional(S.NullOr(S.Array(ToolDefinitionJson))), + top_p: S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(1))).pipe(S.optional, S.withDecodingDefault(() => 1 as const)), + debug: S.optional(S.NullOr(S.Struct({ + echo_upstream_body: S.optional(S.NullOr(S.Boolean)), + }))), + image_config: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), + modalities: S.optional(S.NullOr(S.Array(S.Literals(["text", "image"])))), +}) {} + +export const ChatCompletionFinishReason = S.Literals(["tool_calls", "stop", "length", "content_filter", - "error", -) {} + "error",]) -export class Schema6 extends S.Union(ChatCompletionFinishReason, S.Null) {} +export const Schema6 = S.Union([ChatCompletionFinishReason, S.Null]) export class ChatMessageTokenLogprob extends S.Class("ChatMessageTokenLogprob")({ token: S.String, @@ -5171,134 +4895,101 @@ export class ChatMessageTokenLogprob extends S.Class("C }) {} export class ChatMessageTokenLogprobs extends S.Class("ChatMessageTokenLogprobs")({ - content: S.optionalWith(S.Array(ChatMessageTokenLogprob), { nullable: true }), - refusal: S.optionalWith(S.Array(ChatMessageTokenLogprob), { nullable: true }), + content: S.optional(S.NullOr(S.Array(ChatMessageTokenLogprob))), + refusal: S.optional(S.NullOr(S.Array(ChatMessageTokenLogprob))), }) {} export class ChatResponseChoice extends S.Class("ChatResponseChoice")({ finish_reason: S.NullOr(ChatCompletionFinishReason), index: S.Number, message: AssistantMessage, - logprobs: S.optionalWith(ChatMessageTokenLogprobs, { nullable: true }), + logprobs: S.optional(S.NullOr(ChatMessageTokenLogprobs)), }) {} export class ChatGenerationTokenUsage extends S.Class("ChatGenerationTokenUsage")({ completion_tokens: S.Number, prompt_tokens: S.Number, total_tokens: S.Number, - completion_tokens_details: S.optionalWith( - S.Struct({ - reasoning_tokens: S.optionalWith(S.Number, { nullable: true }), - audio_tokens: S.optionalWith(S.Number, { nullable: true }), - accepted_prediction_tokens: S.optionalWith(S.Number, { nullable: true }), - rejected_prediction_tokens: S.optionalWith(S.Number, { nullable: true }), - }), - { nullable: true }, - ), - prompt_tokens_details: S.optionalWith( - S.Struct({ - cached_tokens: S.optionalWith(S.Number, { nullable: true }), - cache_write_tokens: S.optionalWith(S.Number, { nullable: true }), - audio_tokens: S.optionalWith(S.Number, { nullable: true }), - video_tokens: S.optionalWith(S.Number, { nullable: true }), - }), - { nullable: true }, - ), - cost: S.optionalWith(S.Number, { nullable: true }), - cost_details: S.optionalWith( - S.Struct({ upstream_inference_cost: S.optionalWith(S.Number, { nullable: true }) }), - { - nullable: true, - }, - ), + completion_tokens_details: S.optional(S.NullOr(S.Struct({ + reasoning_tokens: S.optional(S.NullOr(S.Number)), + audio_tokens: S.optional(S.NullOr(S.Number)), + accepted_prediction_tokens: S.optional(S.NullOr(S.Number)), + rejected_prediction_tokens: S.optional(S.NullOr(S.Number)), + }))), + prompt_tokens_details: S.optional(S.NullOr(S.Struct({ + cached_tokens: S.optional(S.NullOr(S.Number)), + cache_write_tokens: S.optional(S.NullOr(S.Number)), + audio_tokens: S.optional(S.NullOr(S.Number)), + video_tokens: S.optional(S.NullOr(S.Number)), + }))), + cost: S.optional(S.NullOr(S.Number)), + cost_details: S.optional(S.NullOr(S.Struct({ upstream_inference_cost: S.optional(S.NullOr(S.Number)) }))), }) {} export class ChatResponse extends S.Class("ChatResponse")({ id: S.String, - provider: S.optionalWith(S.String, { nullable: true }), + provider: S.optional(S.NullOr(S.String)), choices: S.Array(ChatResponseChoice), created: S.Number, model: S.String, object: S.Literal("chat.completion"), - system_fingerprint: S.optionalWith(S.String, { nullable: true }), - usage: S.optionalWith(ChatGenerationTokenUsage, { nullable: true }), + system_fingerprint: S.optional(S.NullOr(S.String)), + usage: S.optional(S.NullOr(ChatGenerationTokenUsage)), }) {} export class ChatError extends S.Class("ChatError")({ error: S.Struct({ - code: S.NullOr(S.Union(S.String, S.Number)), + code: S.NullOr(S.Union([S.String, S.Number])), message: S.String, - param: S.optionalWith(S.String, { nullable: true }), - type: S.optionalWith(S.String, { nullable: true }), + param: S.optional(S.NullOr(S.String)), + type: S.optional(S.NullOr(S.String)), }), }) {} export class CompletionCreateParams extends S.Class("CompletionCreateParams")({ - model: S.optionalWith(ModelName, { nullable: true }), - models: S.optionalWith(S.Array(ModelName), { nullable: true }), - prompt: S.Union(S.String, S.Array(S.String), S.Array(S.Number), S.Array(S.Array(S.Number))), - best_of: S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(20)), { - nullable: true, - }), - echo: S.optionalWith(S.Boolean, { nullable: true }), - frequency_penalty: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { - nullable: true, - }), - logit_bias: S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), - logprobs: S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(5)), { - nullable: true, - }), - max_tokens: S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(9007199254740991)), { - nullable: true, - }), - n: S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(1), S.lessThanOrEqualTo(128)), { nullable: true }), - presence_penalty: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { - nullable: true, - }), - seed: S.optionalWith( - S.Int.pipe(S.greaterThanOrEqualTo(-9007199254740991), S.lessThanOrEqualTo(9007199254740991)), - { - nullable: true, - }, - ), - stop: S.optionalWith(S.Union(S.String, S.Array(S.String)), { nullable: true }), - stream: S.optionalWith(S.Boolean, { nullable: true, default: () => false as const }), - stream_options: S.optionalWith( - S.Struct({ - include_usage: S.optionalWith(S.Boolean, { nullable: true }), - }), - { nullable: true }, - ), - suffix: S.optionalWith(S.String, { nullable: true }), - temperature: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2)), { - nullable: true, - }), - top_p: S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(1)), { - nullable: true, - }), - user: S.optionalWith(S.String, { nullable: true }), - metadata: S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), - response_format: S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + model: S.optional(S.NullOr(ModelName)), + models: S.optional(S.NullOr(S.Array(ModelName))), + prompt: S.Union([S.String, S.Array(S.String), S.Array(S.Number), S.Array(S.Array(S.Number))]), + best_of: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(1), S.isLessThanOrEqualTo(20)))), + echo: S.optional(S.NullOr(S.Boolean)), + frequency_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), + logit_bias: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), + logprobs: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(5)))), + max_tokens: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(1), S.isLessThanOrEqualTo(9007199254740991)))), + n: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(1), S.isLessThanOrEqualTo(128)))), + presence_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), + seed: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(-9007199254740991), S.isLessThanOrEqualTo(9007199254740991)))), + stop: S.optional(S.NullOr(S.Union([S.String, S.Array(S.String)]))), + stream: S.NullOr(S.Boolean).pipe(S.optional, S.withDecodingDefault(() => false as const)), + stream_options: S.optional(S.NullOr(S.Struct({ + include_usage: S.optional(S.NullOr(S.Boolean)), + }))), + suffix: S.optional(S.NullOr(S.String)), + temperature: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(2)))), + top_p: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(1)))), + user: S.optional(S.NullOr(S.String)), + metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), + response_format: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), }) {} export class CompletionLogprobs extends S.Class("CompletionLogprobs")({ tokens: S.Array(S.String), token_logprobs: S.Array(S.Number), - top_logprobs: S.NullOr(S.Array(S.Record({ key: S.String, value: S.Unknown }))), + top_logprobs: S.NullOr(S.Array(S.Record(S.String, S.Unknown))), text_offset: S.Array(S.Number), }) {} -export class CompletionFinishReasonEnum extends S.Literal("stop", "length", "content_filter") {} +export const CompletionFinishReasonEnum = S.Literals(["stop", "length", "content_filter"]) -export class CompletionFinishReason extends S.Union(CompletionFinishReasonEnum, S.Null) {} +export const CompletionFinishReason = S.Union([CompletionFinishReasonEnum, S.Null]) export class CompletionChoice extends S.Class("CompletionChoice")({ text: S.String, index: S.Number, logprobs: S.NullOr(CompletionLogprobs), - finish_reason: S.NullOr(S.Literal("stop", "length", "content_filter")), - native_finish_reason: S.optionalWith(S.String, { nullable: true }), - reasoning: S.optionalWith(S.String, { nullable: true }), + finish_reason: S.NullOr(S.Literals(["stop", "length", "content_filter"])), + native_finish_reason: S.optional(S.NullOr(S.String)), + reasoning: S.optional(S.NullOr(S.String)), }) {} export class CompletionUsage extends S.Class("CompletionUsage")({ @@ -5312,10 +5003,10 @@ export class CompletionResponse extends S.Class("CompletionR object: S.Literal("text_completion"), created: S.Number, model: S.String, - provider: S.optionalWith(S.String, { nullable: true }), - system_fingerprint: S.optionalWith(S.String, { nullable: true }), + provider: S.optional(S.NullOr(S.String)), + system_fingerprint: S.optional(S.NullOr(S.String)), choices: S.Array(CompletionChoice), - usage: S.optionalWith(CompletionUsage, { nullable: true }), + usage: S.optional(S.NullOr(CompletionUsage)), }) {} export const make = ( @@ -5331,18 +5022,17 @@ export const make = ( Effect.orElseSucceed(response.json, () => "Unexpected status code"), (description) => Effect.fail( - new HttpClientError.ResponseError({ + new HttpClientError.StatusCodeError({ request: response.request, response, - reason: "StatusCode", description: typeof description === "string" ? description : JSON.stringify(description), }), ), ) - const withResponse: ( - f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect, - ) => (request: HttpClientRequest.HttpClientRequest) => Effect.Effect = options.transformClient + const withResponse: ( + f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect, + ) => (request: HttpClientRequest.HttpClientRequest) => Effect.Effect = options.transformClient ? (f) => (request) => Effect.flatMap( Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), @@ -5350,20 +5040,20 @@ export const make = ( ) : (f) => (request) => Effect.flatMap(httpClient.execute(request), f) const decodeSuccess = - (schema: S.Schema) => + (schema: S.Top) => (response: HttpClientResponse.HttpClientResponse) => HttpClientResponse.schemaBodyJson(schema)(response) const decodeError = - (tag: Tag, schema: S.Schema) => + (tag: Tag, schema: S.Top) => (response: HttpClientResponse.HttpClientResponse) => - Effect.flatMap(HttpClientResponse.schemaBodyJson(schema)(response), (cause) => + Effect.flatMap(HttpClientResponse.schemaBodyJson(schema)(response), (cause: any) => Effect.fail(ClientError(tag, cause, response)), ) - return { + return ({ httpClient, - createResponses: (options) => + createResponses: (options: any) => HttpClientRequest.post(`/responses`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(OpenResponsesNonStreamingResponse), @@ -5384,9 +5074,9 @@ export const make = ( }), ), ), - createMessages: (options) => + createMessages: (options: any) => HttpClientRequest.post(`/messages`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(AnthropicMessagesResponse), @@ -5402,7 +5092,7 @@ export const make = ( }), ), ), - getUserActivity: (options) => + getUserActivity: (options: any) => HttpClientRequest.get(`/activity`).pipe( HttpClientRequest.setUrlParams({ date: options?.["date"] as any }), withResponse( @@ -5428,9 +5118,9 @@ export const make = ( }), ), ), - createCoinbaseCharge: (options) => + createCoinbaseCharge: (options: any) => HttpClientRequest.post(`/credits/coinbase`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(CreateCoinbaseCharge200), @@ -5442,9 +5132,9 @@ export const make = ( }), ), ), - createEmbeddings: (options) => + createEmbeddings: (options: any) => HttpClientRequest.post(`/embeddings`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(CreateEmbeddings200), @@ -5473,7 +5163,7 @@ export const make = ( }), ), ), - getGeneration: (options) => + getGeneration: (options: any) => HttpClientRequest.get(`/generation`).pipe( HttpClientRequest.setUrlParams({ id: options?.["id"] as any }), withResponse( @@ -5501,7 +5191,7 @@ export const make = ( }), ), ), - getModels: (options) => + getModels: (options: any) => HttpClientRequest.get(`/models`).pipe( HttpClientRequest.setUrlParams({ category: options?.["category"] as any, @@ -5527,7 +5217,7 @@ export const make = ( }), ), ), - listEndpoints: (author, slug) => + listEndpoints: (author: any, slug: any) => HttpClientRequest.get(`/models/${author}/${slug}/endpoints`).pipe( withResponse( HttpClientResponse.matchStatus({ @@ -5558,7 +5248,7 @@ export const make = ( }), ), ), - list: (options) => + list: (options: any) => HttpClientRequest.get(`/keys`).pipe( HttpClientRequest.setUrlParams({ include_disabled: options?.["include_disabled"] as any, @@ -5574,9 +5264,9 @@ export const make = ( }), ), ), - createKeys: (options) => + createKeys: (options: any) => HttpClientRequest.post(`/keys`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(CreateKeys201), @@ -5588,7 +5278,7 @@ export const make = ( }), ), ), - getKey: (hash) => + getKey: (hash: any) => HttpClientRequest.get(`/keys/${hash}`).pipe( withResponse( HttpClientResponse.matchStatus({ @@ -5601,8 +5291,8 @@ export const make = ( }), ), ), - deleteKeys: (hash) => - HttpClientRequest.del(`/keys/${hash}`).pipe( + deleteKeys: (hash: any) => + HttpClientRequest.delete(`/keys/${hash}`).pipe( withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(DeleteKeys200), @@ -5614,9 +5304,9 @@ export const make = ( }), ), ), - updateKeys: (hash, options) => + updateKeys: (hash: any, options: any) => HttpClientRequest.patch(`/keys/${hash}`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(UpdateKeys200), @@ -5629,7 +5319,7 @@ export const make = ( }), ), ), - listGuardrails: (options) => + listGuardrails: (options: any) => HttpClientRequest.get(`/guardrails`).pipe( HttpClientRequest.setUrlParams({ offset: options?.["offset"] as any, @@ -5644,9 +5334,9 @@ export const make = ( }), ), ), - createGuardrail: (options) => + createGuardrail: (options: any) => HttpClientRequest.post(`/guardrails`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(CreateGuardrail201), @@ -5657,7 +5347,7 @@ export const make = ( }), ), ), - getGuardrail: (id) => + getGuardrail: (id: any) => HttpClientRequest.get(`/guardrails/${id}`).pipe( withResponse( HttpClientResponse.matchStatus({ @@ -5669,8 +5359,8 @@ export const make = ( }), ), ), - deleteGuardrail: (id) => - HttpClientRequest.del(`/guardrails/${id}`).pipe( + deleteGuardrail: (id: any) => + HttpClientRequest.delete(`/guardrails/${id}`).pipe( withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(DeleteGuardrail200), @@ -5681,9 +5371,9 @@ export const make = ( }), ), ), - updateGuardrail: (id, options) => + updateGuardrail: (id: any, options: any) => HttpClientRequest.patch(`/guardrails/${id}`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(UpdateGuardrail200), @@ -5695,7 +5385,7 @@ export const make = ( }), ), ), - listKeyAssignments: (options) => + listKeyAssignments: (options: any) => HttpClientRequest.get(`/guardrails/assignments/keys`).pipe( HttpClientRequest.setUrlParams({ offset: options?.["offset"] as any, @@ -5710,7 +5400,7 @@ export const make = ( }), ), ), - listMemberAssignments: (options) => + listMemberAssignments: (options: any) => HttpClientRequest.get(`/guardrails/assignments/members`).pipe( HttpClientRequest.setUrlParams({ offset: options?.["offset"] as any, @@ -5725,7 +5415,7 @@ export const make = ( }), ), ), - listGuardrailKeyAssignments: (id, options) => + listGuardrailKeyAssignments: (id: any, options: any) => HttpClientRequest.get(`/guardrails/${id}/assignments/keys`).pipe( HttpClientRequest.setUrlParams({ offset: options?.["offset"] as any, @@ -5741,9 +5431,9 @@ export const make = ( }), ), ), - bulkAssignKeysToGuardrail: (id, options) => + bulkAssignKeysToGuardrail: (id: any, options: any) => HttpClientRequest.post(`/guardrails/${id}/assignments/keys`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(BulkAssignKeysToGuardrail200), @@ -5755,7 +5445,7 @@ export const make = ( }), ), ), - listGuardrailMemberAssignments: (id, options) => + listGuardrailMemberAssignments: (id: any, options: any) => HttpClientRequest.get(`/guardrails/${id}/assignments/members`).pipe( HttpClientRequest.setUrlParams({ offset: options?.["offset"] as any, @@ -5771,9 +5461,9 @@ export const make = ( }), ), ), - bulkAssignMembersToGuardrail: (id, options) => + bulkAssignMembersToGuardrail: (id: any, options: any) => HttpClientRequest.post(`/guardrails/${id}/assignments/members`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(BulkAssignMembersToGuardrail200), @@ -5785,9 +5475,9 @@ export const make = ( }), ), ), - bulkUnassignKeysFromGuardrail: (id, options) => + bulkUnassignKeysFromGuardrail: (id: any, options: any) => HttpClientRequest.post(`/guardrails/${id}/assignments/keys/remove`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(BulkUnassignKeysFromGuardrail200), @@ -5799,9 +5489,9 @@ export const make = ( }), ), ), - bulkUnassignMembersFromGuardrail: (id, options) => + bulkUnassignMembersFromGuardrail: (id: any, options: any) => HttpClientRequest.post(`/guardrails/${id}/assignments/members/remove`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(BulkUnassignMembersFromGuardrail200), @@ -5824,9 +5514,9 @@ export const make = ( }), ), ), - exchangeAuthCodeForAPIKey: (options) => + exchangeAuthCodeForAPIKey: (options: any) => HttpClientRequest.post(`/auth/keys`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(ExchangeAuthCodeForAPIKey200), @@ -5837,9 +5527,9 @@ export const make = ( }), ), ), - createAuthKeysCode: (options) => + createAuthKeysCode: (options: any) => HttpClientRequest.post(`/auth/keys/code`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(CreateAuthKeysCode200), @@ -5850,9 +5540,9 @@ export const make = ( }), ), ), - sendChatCompletionRequest: (options) => + sendChatCompletionRequest: (options: any) => HttpClientRequest.post(`/chat/completions`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(ChatResponse), @@ -5864,9 +5554,9 @@ export const make = ( }), ), ), - createCompletions: (options) => + createCompletions: (options: any) => HttpClientRequest.post(`/completions`).pipe( - HttpClientRequest.bodyUnsafeJson(options), + HttpClientRequest.bodyJsonUnsafe(options), withResponse( HttpClientResponse.matchStatus({ "2xx": decodeSuccess(CompletionResponse), @@ -5878,7 +5568,7 @@ export const make = ( }), ), ), - } + }) as any as Client } export interface Client { @@ -5891,7 +5581,7 @@ export interface Client { ) => Effect.Effect< typeof OpenResponsesNonStreamingResponse.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"PaymentRequiredResponse", typeof PaymentRequiredResponse.Type> @@ -5914,7 +5604,7 @@ export interface Client { ) => Effect.Effect< typeof AnthropicMessagesResponse.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"CreateMessages400", typeof CreateMessages400.Type> | ClientError<"CreateMessages401", typeof CreateMessages401.Type> | ClientError<"CreateMessages403", typeof CreateMessages403.Type> @@ -5932,7 +5622,7 @@ export interface Client { ) => Effect.Effect< typeof GetUserActivity200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"ForbiddenResponse", typeof ForbiddenResponse.Type> @@ -5944,7 +5634,7 @@ export interface Client { readonly getCredits: () => Effect.Effect< typeof GetCredits200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"ForbiddenResponse", typeof ForbiddenResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> @@ -5957,7 +5647,7 @@ export interface Client { ) => Effect.Effect< typeof CreateCoinbaseCharge200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> @@ -5971,7 +5661,7 @@ export interface Client { ) => Effect.Effect< typeof CreateEmbeddings200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"PaymentRequiredResponse", typeof PaymentRequiredResponse.Type> @@ -5989,7 +5679,7 @@ export interface Client { readonly listEmbeddingsModels: () => Effect.Effect< typeof ModelsListResponse.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > @@ -6001,7 +5691,7 @@ export interface Client { ) => Effect.Effect< typeof GetGeneration200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"PaymentRequiredResponse", typeof PaymentRequiredResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> @@ -6017,7 +5707,7 @@ export interface Client { readonly listModelsCount: () => Effect.Effect< typeof ModelsCountResponse.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > /** @@ -6028,7 +5718,7 @@ export interface Client { ) => Effect.Effect< typeof ModelsListResponse.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > @@ -6038,7 +5728,7 @@ export interface Client { readonly listModelsUser: () => Effect.Effect< typeof ModelsListResponse.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > @@ -6051,7 +5741,7 @@ export interface Client { ) => Effect.Effect< typeof ListEndpoints200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > @@ -6061,7 +5751,7 @@ export interface Client { readonly listEndpointsZdr: () => Effect.Effect< typeof ListEndpointsZdr200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > /** @@ -6070,7 +5760,7 @@ export interface Client { readonly listProviders: () => Effect.Effect< typeof ListProviders200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > /** @@ -6081,7 +5771,7 @@ export interface Client { ) => Effect.Effect< typeof List200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> @@ -6094,7 +5784,7 @@ export interface Client { ) => Effect.Effect< typeof CreateKeys201.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> @@ -6108,7 +5798,7 @@ export interface Client { ) => Effect.Effect< typeof GetKey200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> @@ -6122,7 +5812,7 @@ export interface Client { ) => Effect.Effect< typeof DeleteKeys200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> @@ -6137,7 +5827,7 @@ export interface Client { ) => Effect.Effect< typeof UpdateKeys200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> @@ -6152,7 +5842,7 @@ export interface Client { ) => Effect.Effect< typeof ListGuardrails200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > @@ -6164,7 +5854,7 @@ export interface Client { ) => Effect.Effect< typeof CreateGuardrail201.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> @@ -6177,7 +5867,7 @@ export interface Client { ) => Effect.Effect< typeof GetGuardrail200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> @@ -6190,7 +5880,7 @@ export interface Client { ) => Effect.Effect< typeof DeleteGuardrail200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> @@ -6204,7 +5894,7 @@ export interface Client { ) => Effect.Effect< typeof UpdateGuardrail200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> @@ -6218,7 +5908,7 @@ export interface Client { ) => Effect.Effect< typeof ListKeyAssignments200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > @@ -6230,7 +5920,7 @@ export interface Client { ) => Effect.Effect< typeof ListMemberAssignments200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > @@ -6243,7 +5933,7 @@ export interface Client { ) => Effect.Effect< typeof ListGuardrailKeyAssignments200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> @@ -6257,7 +5947,7 @@ export interface Client { ) => Effect.Effect< typeof BulkAssignKeysToGuardrail200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> @@ -6272,7 +5962,7 @@ export interface Client { ) => Effect.Effect< typeof ListGuardrailMemberAssignments200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> @@ -6286,7 +5976,7 @@ export interface Client { ) => Effect.Effect< typeof BulkAssignMembersToGuardrail200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> @@ -6301,7 +5991,7 @@ export interface Client { ) => Effect.Effect< typeof BulkUnassignKeysFromGuardrail200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> @@ -6316,7 +6006,7 @@ export interface Client { ) => Effect.Effect< typeof BulkUnassignMembersFromGuardrail200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> @@ -6328,7 +6018,7 @@ export interface Client { readonly getCurrentKey: () => Effect.Effect< typeof GetCurrentKey200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > @@ -6340,7 +6030,7 @@ export interface Client { ) => Effect.Effect< typeof ExchangeAuthCodeForAPIKey200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"ForbiddenResponse", typeof ForbiddenResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> @@ -6353,7 +6043,7 @@ export interface Client { ) => Effect.Effect< typeof CreateAuthKeysCode200.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> @@ -6366,7 +6056,7 @@ export interface Client { ) => Effect.Effect< typeof ChatResponse.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"ChatError", typeof ChatError.Type> | ClientError<"ChatError", typeof ChatError.Type> | ClientError<"ChatError", typeof ChatError.Type> @@ -6380,7 +6070,7 @@ export interface Client { ) => Effect.Effect< typeof CompletionResponse.Type, | HttpClientError.HttpClientError - | ParseError + | S.SchemaError | ClientError<"ChatError", typeof ChatError.Type> | ClientError<"ChatError", typeof ChatError.Type> | ClientError<"ChatError", typeof ChatError.Type> diff --git a/libs/bot-sdk/src/hazel-bot-sdk.ts b/libs/bot-sdk/src/hazel-bot-sdk.ts index ffb4bb7c7..34f857511 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.ts @@ -371,7 +371,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB const decodeCommandArgs = (event: Extract) => Option.match( Option.flatMap(commandGroup, (group) => - Option.fromNullable( + Option.fromNullishOr( group.commands.find((c: CommandDef) => c.name === event.payload.commandName), ), ), diff --git a/libs/bot-sdk/src/retry.ts b/libs/bot-sdk/src/retry.ts index 739d41686..099c729fb 100644 --- a/libs/bot-sdk/src/retry.ts +++ b/libs/bot-sdk/src/retry.ts @@ -30,7 +30,7 @@ export const RetryStrategy = { ? retryPolicyForTag(tag) === "transient" || retryPolicyForTag(tag) === "connection" : isRetryableError(error) }), - Schedule.intersect(Schedule.recurs(5)), + Schedule.both(Schedule.recurs(5)), ), /** @@ -49,7 +49,7 @@ export const RetryStrategy = { : "" return tag.length > 0 ? retryPolicyForTag(tag) === "connection" : isRetryableError(error) }), - Schedule.intersect(Schedule.recurs(10)), + Schedule.both(Schedule.recurs(10)), ), /** @@ -67,7 +67,7 @@ export const RetryStrategy = { : "" return tag.length > 0 ? retryPolicyForTag(tag) === "quick" : isRetryableError(error) }), - Schedule.intersect(Schedule.recurs(3)), + Schedule.both(Schedule.recurs(3)), ), /** @@ -111,4 +111,4 @@ export const RetryStrategy = { export const composeRetryStrategies = ( first: Schedule.Schedule, second: Schedule.Schedule, -) => Schedule.intersect(first, second) +) => Schedule.both(first, second) diff --git a/packages/actors/src/actors/message-actor.ts b/packages/actors/src/actors/message-actor.ts index a37f0ed84..df6c3fa42 100644 --- a/packages/actors/src/actors/message-actor.ts +++ b/packages/actors/src/actors/message-actor.ts @@ -1,7 +1,14 @@ import { Action, CreateConnState, Log, actor } from "@hazel/rivet-effect" import { Effect } from "effect" import { UserError } from "rivetkit" -import { TokenValidationService, type ActorConnectParams } from "../auth" +import { + TokenValidationService, + type ActorConnectParams, + type InvalidTokenFormatError, + type JwtValidationError, + type BotTokenValidationError, + type ConfigError, +} from "../auth" import { messageActorRuntime } from "../effect/runtime" const getTokenKind = (token: string): "bot" | "jwt" | "unknown" => { @@ -123,7 +130,7 @@ export const messageActor = actor({ return yield* service.validateToken(params.token) }).pipe( Effect.catchTags({ - InvalidTokenFormatError: (e) => + InvalidTokenFormatError: (e: InvalidTokenFormatError) => Log.error("Token validation failed: invalid format", { tokenKind: getTokenKind(params.token), tokenPrefix: params.token.slice(0, 12), @@ -132,7 +139,7 @@ export const messageActor = actor({ Effect.fail(new UserError(e.message, { code: "invalid_token" })), ), ), - JwtValidationError: (e) => + JwtValidationError: (e: JwtValidationError) => Log.error("Token validation failed: JWT error", { error: e.message, tokenKind: getTokenKind(params.token), @@ -141,7 +148,7 @@ export const messageActor = actor({ Effect.fail(new UserError(e.message, { code: "invalid_token" })), ), ), - BotTokenValidationError: (e) => + BotTokenValidationError: (e: BotTokenValidationError) => Log.error("Token validation failed: bot token error", { statusCode: e.statusCode, tokenKind: getTokenKind(params.token), @@ -152,7 +159,7 @@ export const messageActor = actor({ Effect.fail(new UserError(e.message, { code: "invalid_token" })), ), ), - ConfigError: (e) => + ConfigError: (e: ConfigError) => Log.error("Token validation failed: auth config unavailable", { error: e.message, tokenKind: getTokenKind(params.token), diff --git a/packages/actors/src/auth/config-service.ts b/packages/actors/src/auth/config-service.ts index 832fb6eed..7d140e36e 100644 --- a/packages/actors/src/auth/config-service.ts +++ b/packages/actors/src/auth/config-service.ts @@ -1,5 +1,5 @@ import type { WorkOSClientId } from "@hazel/schema" -import { ServiceMap, Config, Effect, Option, Redacted, Schema } from "effect" +import { ServiceMap, Config, Effect, Layer, Option, Redacted, Schema } from "effect" import { WorkOSClientId as WorkOSClientIdSchema } from "@hazel/schema" /** @@ -24,30 +24,33 @@ const optionalValue = (effect: Effect.Effect) => effect.pipe(E export class TokenValidationConfigService extends ServiceMap.Service()( "TokenValidationConfigService", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const workosClientId = yield* optionalValue( - Config.string("WORKOS_CLIENT_ID").pipe( - Effect.flatMap((value) => Schema.decodeUnknown(WorkOSClientIdSchema)(value)), + Effect.flatMap( + Config.string("WORKOS_CLIENT_ID").asEffect(), + (value) => Schema.decodeUnknownEffect(WorkOSClientIdSchema)(value), ), ) const backendUrl = yield* optionalValue( Config.string("BACKEND_URL").pipe( - Effect.orElse(() => Config.string("API_BASE_URL")), - Effect.orElse(() => Config.string("VITE_BACKEND_URL")), - Effect.orElse(() => Config.string("VITE_API_BASE_URL")), - ), + Config.orElse(() => Config.string("API_BASE_URL")), + Config.orElse(() => Config.string("VITE_BACKEND_URL")), + Config.orElse(() => Config.string("VITE_API_BASE_URL")), + ).asEffect(), ) - const internalSecret = yield* optionalValue(Config.redacted("INTERNAL_SECRET")) + const internalSecret = yield* optionalValue(Config.redacted("INTERNAL_SECRET").asEffect()) const config: TokenValidationConfig = { - workosClientId, - backendUrl, - internalSecret, + workosClientId: workosClientId as Option.Option, + backendUrl: backendUrl as Option.Option, + internalSecret: internalSecret as Option.Option, } return config }), }, -) {} +) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/actors/src/auth/jwks-service.test.ts b/packages/actors/src/auth/jwks-service.test.ts index dbb6bc9c1..928b2dccc 100644 --- a/packages/actors/src/auth/jwks-service.test.ts +++ b/packages/actors/src/auth/jwks-service.test.ts @@ -1,4 +1,4 @@ -import { Effect, Either } from "effect" +import { Effect, Result } from "effect" import { afterEach, describe, expect, it } from "vitest" import { JwksService } from "./jwks-service" @@ -32,12 +32,12 @@ describe("JwksService", () => { Effect.gen(function* () { const service = yield* JwksService return yield* service.getJwks() - }).pipe(Effect.provide(JwksService.layer), Effect.either), + }).pipe(Effect.provide(JwksService.layer), Effect.result), ) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left._tag).toBe("ConfigError") + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { + expect(result.failure._tag).toBe("ConfigError") } }) diff --git a/packages/actors/src/auth/token-validation-service.ts b/packages/actors/src/auth/token-validation-service.ts index 59286783d..c69a62b7d 100644 --- a/packages/actors/src/auth/token-validation-service.ts +++ b/packages/actors/src/auth/token-validation-service.ts @@ -1,7 +1,6 @@ import { HttpClient, HttpClientRequest } from "effect/unstable/http" import { WorkOSJwtClaims, WorkOSRole } from "@hazel/schema" -import { ServiceMap, Either, Effect, Layer, Option, Redacted, Schema } from "effect" -import { TreeFormatter } from "effect/ParseResult" +import { ServiceMap, Result, Effect, Layer, Option, Redacted, Schema } from "effect" import type { JWTPayload } from "jose" import { jwtVerify } from "jose" import { TokenValidationConfigService } from "./config-service" @@ -42,11 +41,11 @@ function isBotToken(token: string): boolean { export class TokenValidationService extends ServiceMap.Service()( "TokenValidationService", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const config = yield* TokenValidationConfigService const jwksService = yield* JwksService - const decodeClaims = Schema.decodeUnknown(WorkOSJwtClaims) - const decodeBotValidationResponse = Schema.decodeUnknown(BotTokenValidationResponseSchema) + const decodeClaims = Schema.decodeUnknownEffect(WorkOSJwtClaims) + const decodeBotValidationResponse = Schema.decodeUnknownEffect(BotTokenValidationResponseSchema) /** * Validate a WorkOS JWT token. @@ -80,21 +79,21 @@ export class TokenValidationService extends ServiceMap.Service Effect.tryPromise(() => jwtVerify(token, jwks, { issuer })).pipe( Effect.map((result) => result.payload as JWTPayloadWithClaims), - Effect.either, + Effect.result, ), { concurrency: 1 }, ).pipe( Effect.flatMap((results) => { - const success = results.find(Either.isRight) + const success = results.find(Result.isSuccess) if (success) { - return Effect.succeed(success.right) + return Effect.succeed(success.success) } return Effect.fail( new JwtValidationError({ message: "Invalid or expired token", cause: results.map((result) => - Either.isLeft(result) ? result.left : null, + Result.isFailure(result) ? result.failure : null, ), }), ) @@ -105,8 +104,7 @@ export class TokenValidationService extends ServiceMap.Service new JwtValidationError({ - message: "Invalid JWT claims", - cause: TreeFormatter.formatErrorSync(error), + message: `Invalid JWT claims: ${error.message}`, }), ), ) @@ -146,31 +144,24 @@ export class TokenValidationService extends ServiceMap.Service requestBase, - onSome: (secret) => + onSome: (secret: Redacted.Redacted) => requestBase.pipe( HttpClientRequest.setHeader("X-Internal-Secret", Redacted.value(secret)), ), }) const response = yield* httpClient.execute(request).pipe( - Effect.catchTag("RequestError", (err) => + Effect.catchTag("HttpClientError", (err) => Effect.fail( new BotTokenValidationError({ message: `Failed to validate bot token: ${err.message}`, }), ), ), - Effect.catchTag("ResponseError", (err) => - Effect.fail( - new BotTokenValidationError({ - message: `Failed to get response: ${err.message}`, - }), - ), - ), ) if (response.status >= 400) { @@ -187,7 +178,7 @@ export class TokenValidationService extends ServiceMap.Service + Effect.catchTag("HttpClientError", (err) => Effect.fail( new BotTokenValidationError({ message: `Failed to parse bot token response: ${err.message}`, @@ -199,7 +190,7 @@ export class TokenValidationService extends ServiceMap.Service new BotTokenValidationError({ - message: `Failed to decode bot token response: ${TreeFormatter.formatErrorSync(error)}`, + message: `Failed to decode bot token response: ${error.message}`, }), ), ) @@ -242,7 +233,7 @@ export class TokenValidationService extends ServiceMap.Service()("@hazel/auth/ ): Effect.Effect => workos.getOrganization(workosOrgId).pipe( Effect.flatMap((org) => - Option.fromNullable(org.externalId).pipe( + Option.fromNullishOr(org.externalId).pipe( Option.match({ onNone: () => Effect.logWarning("WorkOS organization is missing externalId", { @@ -127,7 +126,7 @@ export class BackendAuth extends ServiceMap.Service()("@hazel/auth/ { workosOrgId, externalId, - error: TreeFormatter.formatErrorSync(error), + error: String(error), }, ).pipe(Effect.as(undefined)), ), @@ -287,7 +286,7 @@ export class BackendAuth extends ServiceMap.Service()("@hazel/auth/ (error) => new InvalidJwtPayloadError({ message: "Invalid JWT claims", - detail: TreeFormatter.formatErrorSync(error), + detail: String(error), }), ), ) diff --git a/packages/auth/src/consumers/proxy-auth.ts b/packages/auth/src/consumers/proxy-auth.ts index 9b76c49af..4085741f8 100644 --- a/packages/auth/src/consumers/proxy-auth.ts +++ b/packages/auth/src/consumers/proxy-auth.ts @@ -7,7 +7,6 @@ import { type WorkOSUserId, } from "@hazel/schema" import { ServiceMap, Effect, Layer, Option, Schema } from "effect" -import { TreeFormatter } from "effect/ParseResult" import { createRemoteJWKSet, jwtVerify } from "jose" import { UserLookupCache } from "../cache/user-lookup-cache.ts" import { WorkOSClient } from "../session/workos-client.ts" @@ -47,7 +46,7 @@ export class ProxyAuth extends ServiceMap.Service()("@hazel/auth/Prox ): Effect.Effect => workos.getOrganization(workosOrgId).pipe( Effect.flatMap((org) => - Option.fromNullable(org.externalId).pipe( + Option.fromNullishOr(org.externalId).pipe( Option.match({ onNone: () => Effect.logWarning("WorkOS organization is missing externalId", { @@ -61,7 +60,7 @@ export class ProxyAuth extends ServiceMap.Service()("@hazel/auth/Prox { workosOrgId, externalId, - error: TreeFormatter.formatErrorSync(error), + error: String(error), }, ).pipe(Effect.as(undefined)), ), @@ -116,7 +115,7 @@ export class ProxyAuth extends ServiceMap.Service()("@hazel/auth/Prox }), ), ) - const userOption = Option.fromNullable(userResult[0]) + const userOption = Option.fromNullishOr(userResult[0]) // Cache successful lookup if (Option.isSome(userOption)) { @@ -164,7 +163,7 @@ export class ProxyAuth extends ServiceMap.Service()("@hazel/auth/Prox (error) => new ProxyAuthenticationError({ message: "Invalid JWT claims", - detail: TreeFormatter.formatErrorSync(error), + detail: String(error), }), ), ) diff --git a/packages/auth/src/session/jwt-decoder.ts b/packages/auth/src/session/jwt-decoder.ts index f06d2291f..916101286 100644 --- a/packages/auth/src/session/jwt-decoder.ts +++ b/packages/auth/src/session/jwt-decoder.ts @@ -1,6 +1,5 @@ import { InvalidJwtPayloadError, JwtPayload } from "@hazel/domain" import { Effect, Schema } from "effect" -import { TreeFormatter } from "effect/ParseResult" import { decodeJwt } from "jose" /** @@ -15,7 +14,7 @@ export const decodeSessionJwt = (accessToken: string): Effect.Effect new InvalidJwtPayloadError({ message: "Invalid JWT payload from WorkOS", - detail: TreeFormatter.formatErrorSync(error), + detail: String(error), }), ), ) diff --git a/packages/backend-core/src/repositories/attachment-repo.ts b/packages/backend-core/src/repositories/attachment-repo.ts index f2b4b2c78..8c59adb57 100644 --- a/packages/backend-core/src/repositories/attachment-repo.ts +++ b/packages/backend-core/src/repositories/attachment-repo.ts @@ -1,6 +1,6 @@ import { ModelRepository, schema } from "@hazel/db" import { Attachment } from "@hazel/domain/models" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" export class AttachmentRepo extends ServiceMap.Service()("AttachmentRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/bot-command-repo.ts b/packages/backend-core/src/repositories/bot-command-repo.ts index dc750c05d..b3d881534 100644 --- a/packages/backend-core/src/repositories/bot-command-repo.ts +++ b/packages/backend-core/src/repositories/bot-command-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, inArray, lt, ModelRepository, schema, type TxFn } fr import type { BotCommandId, BotId } from "@hazel/schema" import { BotCommand } from "@hazel/domain/models" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" export class BotCommandRepo extends ServiceMap.Service()("BotCommandRepo", { make: Effect.gen(function* () { @@ -76,7 +76,7 @@ export class BotCommandRepo extends ServiceMap.Service()("BotCom .limit(1), ), )({ botId, name }, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Upsert command (for bot sync) const upsert = ( diff --git a/packages/backend-core/src/repositories/bot-installation-repo.ts b/packages/backend-core/src/repositories/bot-installation-repo.ts index 6c2227fdd..7a9441a9c 100644 --- a/packages/backend-core/src/repositories/bot-installation-repo.ts +++ b/packages/backend-core/src/repositories/bot-installation-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, inArray, ModelRepository, schema, type TxFn } from " import type { BotId, BotInstallationId, OrganizationId } from "@hazel/schema" import { BotInstallation } from "@hazel/domain/models" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" export class BotInstallationRepo extends ServiceMap.Service()("BotInstallationRepo", { make: Effect.gen(function* () { @@ -55,7 +55,7 @@ export class BotInstallationRepo extends ServiceMap.Service .limit(1), ), )({ botId, organizationId }, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Check if a bot is installed in an organization const isInstalled = (botId: BotId, organizationId: OrganizationId, tx?: TxFn) => diff --git a/packages/backend-core/src/repositories/bot-repo.ts b/packages/backend-core/src/repositories/bot-repo.ts index 6f76b8582..33b81f0f7 100644 --- a/packages/backend-core/src/repositories/bot-repo.ts +++ b/packages/backend-core/src/repositories/bot-repo.ts @@ -36,7 +36,7 @@ export class BotRepo extends ServiceMap.Service()("BotRepo", { .limit(1), ), )({ id }, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Find bot by token hash const findByTokenHash = (tokenHash: string, tx?: TxFn) => @@ -55,7 +55,7 @@ export class BotRepo extends ServiceMap.Service()("BotRepo", { .limit(1), ), )({ tokenHash }, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Find bot by user ID const findByUserId = (userId: UserId, tx?: TxFn) => @@ -74,7 +74,7 @@ export class BotRepo extends ServiceMap.Service()("BotRepo", { .limit(1), ), )({ userId }, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Find all bots created by a user const findByCreator = (createdBy: UserId, tx?: TxFn) => diff --git a/packages/backend-core/src/repositories/channel-member-repo.ts b/packages/backend-core/src/repositories/channel-member-repo.ts index d6898b653..53777f1b4 100644 --- a/packages/backend-core/src/repositories/channel-member-repo.ts +++ b/packages/backend-core/src/repositories/channel-member-repo.ts @@ -34,7 +34,7 @@ export class ChannelMemberRepo extends ServiceMap.Service()(" .limit(1), ), )({ channelId, userId }, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Find existing single DM channel between two users const findExistingSingleDmChannel = ( @@ -78,7 +78,7 @@ export class ChannelMemberRepo extends ServiceMap.Service()(" .limit(1), ), )({ userId1, userId2, organizationId }, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]?.channel))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]?.channel))) const listByChannel = (channelId: ChannelId, tx?: TxFn) => db.makeQuery((execute, input: ChannelId) => diff --git a/packages/backend-core/src/repositories/channel-repo.ts b/packages/backend-core/src/repositories/channel-repo.ts index a4332419b..7d5f03593 100644 --- a/packages/backend-core/src/repositories/channel-repo.ts +++ b/packages/backend-core/src/repositories/channel-repo.ts @@ -29,7 +29,7 @@ export class ChannelRepo extends ServiceMap.Service()("ChannelRepo" .limit(1), ), )({ organizationId, name }, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) return { ...baseRepo, diff --git a/packages/backend-core/src/repositories/channel-section-repo.ts b/packages/backend-core/src/repositories/channel-section-repo.ts index 8ac5e128e..c21dc37cd 100644 --- a/packages/backend-core/src/repositories/channel-section-repo.ts +++ b/packages/backend-core/src/repositories/channel-section-repo.ts @@ -1,6 +1,6 @@ import { ModelRepository, schema } from "@hazel/db" import { ChannelSection } from "@hazel/domain/models" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" export class ChannelSectionRepo extends ServiceMap.Service()("ChannelSectionRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/channel-webhook-repo.ts b/packages/backend-core/src/repositories/channel-webhook-repo.ts index a0856aa97..1560c499f 100644 --- a/packages/backend-core/src/repositories/channel-webhook-repo.ts +++ b/packages/backend-core/src/repositories/channel-webhook-repo.ts @@ -49,7 +49,7 @@ export class ChannelWebhookRepo extends ServiceMap.Service() .limit(1), ), )({ tokenHash }, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Update last used timestamp const updateLastUsed = (id: ChannelWebhookId, tx?: TxFn) => diff --git a/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts b/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts index e5d7a7795..4907b2663 100644 --- a/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts @@ -46,7 +46,7 @@ export class ChatSyncChannelLinkRepo extends ServiceMap.Service - Option.fromNullable(results[0]).pipe(decodeChannelLinkOption), + Option.fromNullishOr(results[0]).pipe(decodeChannelLinkOption), ), ) @@ -127,7 +127,7 @@ export class ChatSyncChannelLinkRepo extends ServiceMap.Service - Option.fromNullable(results[0]).pipe(decodeChannelLinkOption), + Option.fromNullishOr(results[0]).pipe(decodeChannelLinkOption), ), ) @@ -167,7 +167,7 @@ export class ChatSyncChannelLinkRepo extends ServiceMap.Service - Option.fromNullable(results[0]).pipe(decodeChannelLinkOption), + Option.fromNullishOr(results[0]).pipe(decodeChannelLinkOption), ), ) diff --git a/packages/backend-core/src/repositories/chat-sync-connection-repo.ts b/packages/backend-core/src/repositories/chat-sync-connection-repo.ts index 347c2ebea..79cf73829 100644 --- a/packages/backend-core/src/repositories/chat-sync-connection-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-connection-repo.ts @@ -70,7 +70,7 @@ export class ChatSyncConnectionRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const findActiveByProvider = (provider: string, tx?: TxFn) => db.makeQuery((execute, data: { provider: string }) => @@ -110,7 +110,7 @@ export class ChatSyncConnectionRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const updateStatus = ( id: SyncConnectionId, diff --git a/packages/backend-core/src/repositories/chat-sync-event-receipt-repo.ts b/packages/backend-core/src/repositories/chat-sync-event-receipt-repo.ts index 064eb0782..881a7d428 100644 --- a/packages/backend-core/src/repositories/chat-sync-event-receipt-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-event-receipt-repo.ts @@ -51,7 +51,7 @@ export class ChatSyncEventReceiptRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const claimByDedupeKey = ( params: { diff --git a/packages/backend-core/src/repositories/chat-sync-message-link-repo.ts b/packages/backend-core/src/repositories/chat-sync-message-link-repo.ts index 8a9a8d92f..db35e33d1 100644 --- a/packages/backend-core/src/repositories/chat-sync-message-link-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-message-link-repo.ts @@ -90,7 +90,7 @@ export class ChatSyncMessageLinkRepo extends ServiceMap.Service - Option.fromNullable(results[0]).pipe(decodeMessageLinkOption), + Option.fromNullishOr(results[0]).pipe(decodeMessageLinkOption), ), ) @@ -130,7 +130,7 @@ export class ChatSyncMessageLinkRepo extends ServiceMap.Service - Option.fromNullable(results[0]).pipe(decodeMessageLinkOption), + Option.fromNullishOr(results[0]).pipe(decodeMessageLinkOption), ), ) @@ -169,7 +169,7 @@ export class ChatSyncMessageLinkRepo extends ServiceMap.Service - Option.fromNullable(results[0]).pipe(decodeMessageLinkOption), + Option.fromNullishOr(results[0]).pipe(decodeMessageLinkOption), ), ) diff --git a/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts b/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts index a2721a404..407e3d6a0 100644 --- a/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts +++ b/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts @@ -33,7 +33,7 @@ export class ConnectConversationChannelRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const findByConversationId = (conversationId: ConnectConversationId, tx?: TxFn) => db.makeQuery((execute, input: ConnectConversationId) => @@ -81,7 +81,7 @@ export class ConnectConversationChannelRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) return { ...baseRepo, diff --git a/packages/backend-core/src/repositories/connect-conversation-repo.ts b/packages/backend-core/src/repositories/connect-conversation-repo.ts index a6ede96e9..c9c11e946 100644 --- a/packages/backend-core/src/repositories/connect-conversation-repo.ts +++ b/packages/backend-core/src/repositories/connect-conversation-repo.ts @@ -33,7 +33,7 @@ export class ConnectConversationRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) return { ...baseRepo, diff --git a/packages/backend-core/src/repositories/connect-invite-repo.ts b/packages/backend-core/src/repositories/connect-invite-repo.ts index 4b13daee1..fbb9fcfc5 100644 --- a/packages/backend-core/src/repositories/connect-invite-repo.ts +++ b/packages/backend-core/src/repositories/connect-invite-repo.ts @@ -31,7 +31,7 @@ export class ConnectInviteRepo extends ServiceMap.Service()(" .limit(1), ), )(id, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const listIncomingForOrganization = (organizationId: OrganizationId, tx?: TxFn) => db.makeQuery((execute, input: OrganizationId) => diff --git a/packages/backend-core/src/repositories/connect-participant-repo.ts b/packages/backend-core/src/repositories/connect-participant-repo.ts index c3777c8b5..e9e614d78 100644 --- a/packages/backend-core/src/repositories/connect-participant-repo.ts +++ b/packages/backend-core/src/repositories/connect-participant-repo.ts @@ -34,7 +34,7 @@ export class ConnectParticipantRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const listByChannel = (channelId: ChannelId, tx?: TxFn) => db.makeQuery((execute, input: ChannelId) => diff --git a/packages/backend-core/src/repositories/custom-emoji-repo.ts b/packages/backend-core/src/repositories/custom-emoji-repo.ts index d7c07cede..a1cde3590 100644 --- a/packages/backend-core/src/repositories/custom-emoji-repo.ts +++ b/packages/backend-core/src/repositories/custom-emoji-repo.ts @@ -29,7 +29,7 @@ export class CustomEmojiRepo extends ServiceMap.Service()("Cust .limit(1), ), )({ organizationId, name }) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const findDeletedByOrgAndName = (organizationId: OrganizationId, name: string) => db @@ -48,7 +48,7 @@ export class CustomEmojiRepo extends ServiceMap.Service()("Cust .limit(1), ), )({ organizationId, name }) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const restore = (id: CustomEmojiId, imageUrl?: string) => db @@ -70,7 +70,7 @@ export class CustomEmojiRepo extends ServiceMap.Service()("Cust .returning(), ), )({ id, imageUrl }) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const softDelete = (id: CustomEmojiId) => db @@ -88,7 +88,7 @@ export class CustomEmojiRepo extends ServiceMap.Service()("Cust .returning(), ), )(id) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) return { ...baseRepo, diff --git a/packages/backend-core/src/repositories/github-subscription-repo.ts b/packages/backend-core/src/repositories/github-subscription-repo.ts index 065fabeee..20fd6ec30 100644 --- a/packages/backend-core/src/repositories/github-subscription-repo.ts +++ b/packages/backend-core/src/repositories/github-subscription-repo.ts @@ -68,7 +68,7 @@ export class GitHubSubscriptionRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Update subscription settings const updateSettings = ( diff --git a/packages/backend-core/src/repositories/integration-connection-repo.ts b/packages/backend-core/src/repositories/integration-connection-repo.ts index 05b132e1d..1b7414996 100644 --- a/packages/backend-core/src/repositories/integration-connection-repo.ts +++ b/packages/backend-core/src/repositories/integration-connection-repo.ts @@ -52,7 +52,7 @@ export class IntegrationConnectionRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Find user-level connection for a specific provider const findUserConnection = ( @@ -90,7 +90,7 @@ export class IntegrationConnectionRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Find user-level connection for a specific provider, including soft-deleted rows. // Used by upsert to reactivate previously disconnected links. @@ -128,7 +128,7 @@ export class IntegrationConnectionRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Find active user-level connection by external account ID. const findActiveUserByExternalAccountId = ( @@ -170,7 +170,7 @@ export class IntegrationConnectionRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Get all connections for an organization (both org-level and user-level) const findAllForOrg = (organizationId: OrganizationId, tx?: TxFn) => @@ -276,7 +276,7 @@ export class IntegrationConnectionRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Find all connections for a GitHub installation ID (stored in metadata JSONB) const findAllByGitHubInstallationId = (installationId: string, tx?: TxFn) => diff --git a/packages/backend-core/src/repositories/integration-token-repo.ts b/packages/backend-core/src/repositories/integration-token-repo.ts index 04c824065..c52ab055b 100644 --- a/packages/backend-core/src/repositories/integration-token-repo.ts +++ b/packages/backend-core/src/repositories/integration-token-repo.ts @@ -28,7 +28,7 @@ export class IntegrationTokenRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Update token (for refresh) const updateToken = ( diff --git a/packages/backend-core/src/repositories/invitation-repo.ts b/packages/backend-core/src/repositories/invitation-repo.ts index 82cbfdbd7..eb448fc4f 100644 --- a/packages/backend-core/src/repositories/invitation-repo.ts +++ b/packages/backend-core/src/repositories/invitation-repo.ts @@ -23,7 +23,7 @@ export class InvitationRepo extends ServiceMap.Service()("Invita .limit(1), ), )(workosInvitationId, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const upsertByWorkosId = (data: Schema.Schema.Type, tx?: TxFn) => db diff --git a/packages/backend-core/src/repositories/message-outbox-repo.ts b/packages/backend-core/src/repositories/message-outbox-repo.ts index 667d3977d..c91827526 100644 --- a/packages/backend-core/src/repositories/message-outbox-repo.ts +++ b/packages/backend-core/src/repositories/message-outbox-repo.ts @@ -192,7 +192,7 @@ export class MessageOutboxRepo extends ServiceMap.Service()(" }) .where(eq(schema.messageOutboxEventsTable.id, eventId)) .returning(), - ).pipe(Effect.map((rows) => Option.fromNullable(rows[0]))), + ).pipe(Effect.map((rows) => Option.fromNullishOr(rows[0]))), )(id, tx) const markRetry = (id: MessageOutboxEventId, params: RetryMessageOutboxEventParams, tx?: TxFn) => @@ -218,7 +218,7 @@ export class MessageOutboxRepo extends ServiceMap.Service()(" }) .where(eq(schema.messageOutboxEventsTable.id, data.id)) .returning(), - ).pipe(Effect.map((rows) => Option.fromNullable(rows[0]))), + ).pipe(Effect.map((rows) => Option.fromNullishOr(rows[0]))), )( { id, @@ -249,7 +249,7 @@ export class MessageOutboxRepo extends ServiceMap.Service()(" }) .where(eq(schema.messageOutboxEventsTable.id, data.id)) .returning(), - ).pipe(Effect.map((rows) => Option.fromNullable(rows[0]))), + ).pipe(Effect.map((rows) => Option.fromNullishOr(rows[0]))), )( { id, diff --git a/packages/backend-core/src/repositories/message-reaction-repo.ts b/packages/backend-core/src/repositories/message-reaction-repo.ts index 8b2a3b36c..716f18456 100644 --- a/packages/backend-core/src/repositories/message-reaction-repo.ts +++ b/packages/backend-core/src/repositories/message-reaction-repo.ts @@ -34,7 +34,7 @@ export class MessageReactionRepo extends ServiceMap.Service .limit(1), ), )({ messageId, userId, emoji }) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const backfillConversationIdForChannel = ( channelId: ChannelId, diff --git a/packages/backend-core/src/repositories/message-repo.ts b/packages/backend-core/src/repositories/message-repo.ts index 6882c2a24..07969ea73 100644 --- a/packages/backend-core/src/repositories/message-repo.ts +++ b/packages/backend-core/src/repositories/message-repo.ts @@ -133,7 +133,7 @@ export class MessageRepo extends ServiceMap.Service()("MessageRepo" .limit(1), ), )(params, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const backfillConversationIdForChannel = ( channelId: ChannelId, diff --git a/packages/backend-core/src/repositories/notification-repo.ts b/packages/backend-core/src/repositories/notification-repo.ts index 58c8dafcd..1f8e64ef7 100644 --- a/packages/backend-core/src/repositories/notification-repo.ts +++ b/packages/backend-core/src/repositories/notification-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, inArray, ModelRepository, schema, type TxFn } from " import type { ChannelId, MessageId, OrganizationMemberId } from "@hazel/schema" import { Notification } from "@hazel/domain/models" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" export class NotificationRepo extends ServiceMap.Service()("NotificationRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/organization-member-repo.ts b/packages/backend-core/src/repositories/organization-member-repo.ts index 6c92ed3b2..5eb578cba 100644 --- a/packages/backend-core/src/repositories/organization-member-repo.ts +++ b/packages/backend-core/src/repositories/organization-member-repo.ts @@ -39,7 +39,7 @@ export class OrganizationMemberRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const upsertByOrgAndUser = ( data: Schema.Schema.Type, @@ -155,7 +155,7 @@ export class OrganizationMemberRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const bulkUpsertByOrgAndUser = ( members: Schema.Schema.Type[], diff --git a/packages/backend-core/src/repositories/organization-repo.ts b/packages/backend-core/src/repositories/organization-repo.ts index d986a52cc..613f2807a 100644 --- a/packages/backend-core/src/repositories/organization-repo.ts +++ b/packages/backend-core/src/repositories/organization-repo.ts @@ -30,7 +30,7 @@ export class OrganizationRepo extends ServiceMap.Service()("Or .limit(1), ), )(slug, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const findBySlugIfPublic = (slug: string, tx?: TxFn) => db @@ -49,7 +49,7 @@ export class OrganizationRepo extends ServiceMap.Service()("Or .limit(1), ), )(slug, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const findAllActive = (tx?: TxFn) => db.makeQuery((execute, _data: {}) => diff --git a/packages/backend-core/src/repositories/pinned-message-repo.ts b/packages/backend-core/src/repositories/pinned-message-repo.ts index 1bcd15530..2f0fc1da3 100644 --- a/packages/backend-core/src/repositories/pinned-message-repo.ts +++ b/packages/backend-core/src/repositories/pinned-message-repo.ts @@ -1,6 +1,6 @@ import { ModelRepository, schema } from "@hazel/db" import { PinnedMessage } from "@hazel/domain/models" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" export class PinnedMessageRepo extends ServiceMap.Service()("PinnedMessageRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/rss-subscription-repo.ts b/packages/backend-core/src/repositories/rss-subscription-repo.ts index b0e3c272a..8ca65699e 100644 --- a/packages/backend-core/src/repositories/rss-subscription-repo.ts +++ b/packages/backend-core/src/repositories/rss-subscription-repo.ts @@ -66,7 +66,7 @@ export class RssSubscriptionRepo extends ServiceMap.Service .limit(1), ), )({ channelId, feedUrl }, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Update subscription settings const updateSettings = ( diff --git a/packages/backend-core/src/repositories/user-presence-status-repo.ts b/packages/backend-core/src/repositories/user-presence-status-repo.ts index 4dcacb866..88cc87041 100644 --- a/packages/backend-core/src/repositories/user-presence-status-repo.ts +++ b/packages/backend-core/src/repositories/user-presence-status-repo.ts @@ -30,7 +30,7 @@ export class UserPresenceStatusRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Upsert user presence status const upsertByUserId = (data: Schema.Schema.Type, tx?: TxFn) => @@ -115,7 +115,7 @@ export class UserPresenceStatusRepo extends ServiceMap.Service Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) // Find users with stale heartbeats (for cron job cleanup) const findStaleUsers = (timeout: Date) => diff --git a/packages/backend-core/src/repositories/user-repo.ts b/packages/backend-core/src/repositories/user-repo.ts index 938d84f0f..2b6fa1e65 100644 --- a/packages/backend-core/src/repositories/user-repo.ts +++ b/packages/backend-core/src/repositories/user-repo.ts @@ -23,7 +23,7 @@ export class UserRepo extends ServiceMap.Service()("UserRepo", { .limit(1), ), )(externalId, tx) - .pipe(Effect.map((results) => Option.fromNullable(results[0]))) + .pipe(Effect.map((results) => Option.fromNullishOr(results[0]))) const findByWorkOSUserId = (workosUserId: WorkOSUserId, tx?: TxFn) => findByExternalId(workosUserId, tx) diff --git a/packages/backend-core/src/services/workos-sync.ts b/packages/backend-core/src/services/workos-sync.ts index 0b3a3bbdf..ea687c10e 100644 --- a/packages/backend-core/src/services/workos-sync.ts +++ b/packages/backend-core/src/services/workos-sync.ts @@ -9,7 +9,6 @@ import { } from "@hazel/schema" import type { Event } from "@workos-inc/node" import { ServiceMap, Effect, Layer, Match, Option, pipe, Schema, Stream } from "effect" -import { TreeFormatter } from "effect/ParseResult" import { InvitationRepo } from "../repositories/invitation-repo" import { OrganizationMemberRepo } from "../repositories/organization-member-repo" import { OrganizationRepo } from "../repositories/organization-repo" @@ -102,7 +101,7 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { (error) => new WorkOSSyncError({ message: "Invalid WorkOS webhook payload", - cause: TreeFormatter.formatErrorSync(error), + cause: String(error), }), ), ) @@ -116,7 +115,7 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { (error) => new WorkOSSyncError({ message: `Invalid WorkOS externalId for organization ${workosOrgId}`, - cause: TreeFormatter.formatErrorSync(error), + cause: String(error), }), ), ) @@ -157,7 +156,7 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { .pipe( Effect.map( (response) => - [response.data, Option.fromNullable(response.listMetadata?.after)] as const, + [response.data, Option.fromNullishOr(response.listMetadata?.after)] as const, ), ), ), @@ -173,7 +172,7 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { .pipe( Effect.map( (response) => - [response.data, Option.fromNullable(response.listMetadata?.after)] as const, + [response.data, Option.fromNullishOr(response.listMetadata?.after)] as const, ), ), ), @@ -198,7 +197,7 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { (response) => [ response.data, - Option.fromNullable(response.listMetadata?.after), + Option.fromNullishOr(response.listMetadata?.after), ] as const, ), ), @@ -226,7 +225,7 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { (response) => [ response.data, - Option.fromNullable(response.listMetadata?.after), + Option.fromNullishOr(response.listMetadata?.after), ] as const, ), ), @@ -453,7 +452,7 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { // Fetch memberships from WorkOS using WorkOS organization ID (with pagination) const workosOrganizationId = yield* decodeWorkOSOrganizationId(workosOrg.id).pipe( - Effect.mapError((error) => String(TreeFormatter.formatErrorSync(error))), + Effect.mapError((error) => String(String(error))), Effect.either, ) if (workosOrganizationId._tag === "Left") { @@ -580,7 +579,7 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { // Fetch invitations from WorkOS using WorkOS organization ID (with pagination) const workosOrganizationId = yield* decodeWorkOSOrganizationId(workosOrg.id).pipe( - Effect.mapError((error) => String(TreeFormatter.formatErrorSync(error))), + Effect.mapError((error) => String(String(error))), Effect.either, ) if (workosOrganizationId._tag === "Left") { diff --git a/packages/db/src/services/database.ts b/packages/db/src/services/database.ts index 1d30fe83c..a6b63d080 100644 --- a/packages/db/src/services/database.ts +++ b/packages/db/src/services/database.ts @@ -1,14 +1,12 @@ import type { ExtractTablesWithRelations } from "drizzle-orm" import type { PgTransaction } from "drizzle-orm/pg-core" import { drizzle, type PostgresJsDatabase, type PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js" -import { Schema } from "effect" +import { Schema, ServiceMap } from "effect" import * as Effect from "effect/Effect" import * as Exit from "effect/Exit" import * as Layer from "effect/Layer" import * as Option from "effect/Option" -import type { ParseError } from "effect/ParseResult" import * as Redacted from "effect/Redacted" -import * as Runtime from "effect/Runtime" import * as Schedule from "effect/Schedule" import postgres from "postgres" import * as schema from "../schema" @@ -31,17 +29,14 @@ export interface TransactionService { readonly execute: TxFn } -export class TransactionContext extends Effect.Tag("TransactionContext")< - TransactionContext, - TransactionService ->() {} +export class TransactionContext extends ServiceMap.Service()("TransactionContext") {} -const DatabaseErrorType = Schema.Literal( +const DatabaseErrorType = Schema.Literals([ "unique_violation", "foreign_key_violation", "connection_error", "query_error", -) +]) export class DatabaseError extends Schema.TaggedErrorClass()("DatabaseError", { type: DatabaseErrorType, @@ -108,8 +103,8 @@ const makeService = (config: Config) => yield* Effect.tryPromise(() => sql`SELECT 1`).pipe( Effect.retry( - Schedule.jitteredWith(Schedule.spaced("1.25 seconds"), { min: 0.5, max: 1.5 }).pipe( - Schedule.intersect(Schedule.recurs(10)), + Schedule.jittered(Schedule.spaced("1.25 seconds"), { min: 0.5, max: 1.5 }).pipe( + Schedule.both(Schedule.recurs(10)), Schedule.tapOutput(([output]) => Effect.logWarning( `[Database client]: Connection to the database failed. Retrying (attempt ${output}).`, @@ -137,10 +132,10 @@ const makeService = (config: Config) => ) const transaction = Effect.fn("Database.transaction")((effect: Effect.Effect) => - Effect.runtime().pipe( - Effect.map((runtime) => Runtime.runPromiseExit(runtime)), + Effect.services().pipe( + Effect.map((services) => Effect.runPromiseExitWith(services)), Effect.flatMap((runPromiseExit) => - Effect.async((resume) => { + Effect.callback((resume) => { db.transaction(async (tx: TransactionClient) => { const txWrapper: TxFn = (fn: (client: TransactionClient) => Promise) => Effect.tryPromise({ @@ -259,6 +254,6 @@ const makeService = (config: Config) => type Shape = Effect.Effect.Success> -export class Database extends Effect.Tag("Database")() {} +export class Database extends ServiceMap.Service()("Database") {} export const layer = (config: Config) => Layer.effect(Database, makeService(config)) diff --git a/packages/db/src/services/drizzle-effect.ts b/packages/db/src/services/drizzle-effect.ts index 7e89fe8c1..1d9060d33 100644 --- a/packages/db/src/services/drizzle-effect.ts +++ b/packages/db/src/services/drizzle-effect.ts @@ -271,17 +271,17 @@ function mapColumnToSchema(column: Drizzle.Column): Schema.Schema { type = Schema.Number } else if (column.dataType === "bigint") { // Check if column has mode: "number" - Drizzle converts to JS number at runtime - type = hasMode(column) && column.mode === "number" ? Schema.Number : Schema.BigIntFromSelf + type = hasMode(column) && column.mode === "number" ? Schema.Number : Schema.BigInt } else if (column.dataType === "boolean") { type = Schema.Boolean } else if (column.dataType === "date") { - type = hasMode(column) && column.mode === "string" ? Schema.String : Schema.DateFromSelf + type = hasMode(column) && column.mode === "string" ? Schema.String : Schema.Date } else if (column.dataType === "string") { // Additional check: if it's a PgTimestamp or PgDate masquerading as string if (Drizzle.is(column, DrizzlePg.PgTimestamp)) { - type = hasMode(column) && column.mode === "string" ? Schema.String : Schema.DateFromSelf + type = hasMode(column) && column.mode === "string" ? Schema.String : Schema.Date } else if (Drizzle.is(column, DrizzlePg.PgDate)) { - type = hasMode(column) && column.mode === "string" ? Schema.String : Schema.DateFromSelf + type = hasMode(column) && column.mode === "string" ? Schema.String : Schema.Date } else { let sType = Schema.String if ( diff --git a/packages/db/src/services/model-repository.ts b/packages/db/src/services/model-repository.ts index c145efe30..67baafbd5 100644 --- a/packages/db/src/services/model-repository.ts +++ b/packages/db/src/services/model-repository.ts @@ -3,7 +3,6 @@ import { eq } from "drizzle-orm" import { pipe } from "effect" import * as Effect from "effect/Effect" import * as Option from "effect/Option" -import type { ParseError } from "effect/ParseResult" import * as Schema from "effect/Schema" import { Database, type DatabaseError, type TxFn } from "./database" import { EntityNotFound, type EntitySchema, type Repository, type RepositoryOptions } from "./model" @@ -78,7 +77,7 @@ export function makeRepository< // @ts-expect-error .where(eq(table[idColumn], id)) .limit(1), - ).pipe(Effect.map((results) => Option.fromNullable(results[0] as RecordType))), + ).pipe(Effect.map((results) => Option.fromNullishOr(results[0] as RecordType))), )(id, tx) as Effect.Effect, DatabaseError> const deleteById = (id: Id, tx?: TxFn) => diff --git a/packages/db/src/services/model.ts b/packages/db/src/services/model.ts index 26def92a1..d4961ddc3 100644 --- a/packages/db/src/services/model.ts +++ b/packages/db/src/services/model.ts @@ -8,7 +8,6 @@ export * from "@hazel/domain/models" import type { EntitySchema } from "@hazel/domain/models" import type * as Effect from "effect/Effect" import type * as Option from "effect/Option" -import type { ParseError } from "effect/ParseResult" import * as Schema from "effect/Schema" import type { DatabaseError, TransactionClient } from "./database" diff --git a/packages/domain/src/http/api.ts b/packages/domain/src/http/api.ts index 6f2a80e5d..39f2ce509 100644 --- a/packages/domain/src/http/api.ts +++ b/packages/domain/src/http/api.ts @@ -32,7 +32,7 @@ export class HazelApi extends HttpApi.make("HazelApp") .add(WebhookGroup) .add(MockDataGroup) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Hazel Chat API", description: "API for the Hazel chat application", version: "1.0.0", diff --git a/packages/domain/src/http/chat-sync.ts b/packages/domain/src/http/chat-sync.ts index 0e9cd9d93..77551e548 100644 --- a/packages/domain/src/http/chat-sync.ts +++ b/packages/domain/src/http/chat-sync.ts @@ -114,7 +114,7 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") error: [ChatSyncConnectionExistsError, ChatSyncIntegrationNotConnectedError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Create Chat Sync Connection", description: "Create a provider-agnostic chat sync connection (Discord, Slack, etc.)", summary: "Create sync connection", @@ -129,7 +129,7 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") error: [UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "List Chat Sync Connections", description: "List chat sync connections for an organization", summary: "List sync connections", @@ -144,7 +144,7 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") error: [ChatSyncConnectionNotFoundError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Delete Chat Sync Connection", description: "Soft-delete a chat sync connection", summary: "Delete sync connection", @@ -160,7 +160,7 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") error: [ChatSyncConnectionNotFoundError, ChatSyncChannelLinkExistsError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Create Chat Sync Channel Link", description: "Link a Hazel channel to an external provider channel", summary: "Create channel link", @@ -175,7 +175,7 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") error: [ChatSyncConnectionNotFoundError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "List Chat Sync Channel Links", description: "List channel links for a sync connection", summary: "List channel links", @@ -190,7 +190,7 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") error: [ChatSyncChannelLinkNotFoundError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Delete Chat Sync Channel Link", description: "Soft-delete a chat sync channel link", summary: "Delete channel link", diff --git a/packages/domain/src/http/incoming-webhooks.ts b/packages/domain/src/http/incoming-webhooks.ts index 5f8424fb2..95e801dbc 100644 --- a/packages/domain/src/http/incoming-webhooks.ts +++ b/packages/domain/src/http/incoming-webhooks.ts @@ -139,7 +139,7 @@ export class IncomingWebhookGroup extends HttpApiGroup.make("incoming-webhooks") error: [WebhookNotFoundError, WebhookDisabledError, InvalidWebhookTokenError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Execute Incoming Webhook", description: "Post a message to a channel via webhook. Supports plain text content and Discord-style embeds.", @@ -159,7 +159,7 @@ export class IncomingWebhookGroup extends HttpApiGroup.make("incoming-webhooks") error: [WebhookNotFoundError, WebhookDisabledError, InvalidWebhookTokenError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Execute OpenStatus Webhook", description: "Receive status alerts from OpenStatus and post them as rich embeds to a channel.", @@ -179,7 +179,7 @@ export class IncomingWebhookGroup extends HttpApiGroup.make("incoming-webhooks") error: [WebhookNotFoundError, WebhookDisabledError, InvalidWebhookTokenError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Execute Railway Webhook", description: "Receive deployment and alert events from Railway and post them as rich embeds to a channel.", diff --git a/packages/domain/src/http/integration-resources.ts b/packages/domain/src/http/integration-resources.ts index ce9240f38..09fae9f23 100644 --- a/packages/domain/src/http/integration-resources.ts +++ b/packages/domain/src/http/integration-resources.ts @@ -167,7 +167,7 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, ResourceNotFoundError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Fetch Linear Issue", description: "Fetch Linear issue details for embedding in chat messages", summary: "Get Linear issue preview data", @@ -183,7 +183,7 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, ResourceNotFoundError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Fetch GitHub PR", description: "Fetch GitHub pull request details for embedding in chat messages", summary: "Get GitHub PR preview data", @@ -202,7 +202,7 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res error: [IntegrationNotConnectedForPreviewError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Get GitHub Repositories", description: "List repositories accessible to the GitHub App installation", summary: "List GitHub repositories", @@ -217,7 +217,7 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Get Discord Guilds", description: "List Discord guilds visible to the connected Discord account", summary: "List Discord guilds", @@ -235,7 +235,7 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Get Discord Guild Channels", description: "List message-capable channels in a Discord guild using the bot token", summary: "List Discord guild channels", diff --git a/packages/domain/src/http/integrations.ts b/packages/domain/src/http/integrations.ts index ecc52cda3..f0219644c 100644 --- a/packages/domain/src/http/integrations.ts +++ b/packages/domain/src/http/integrations.ts @@ -79,7 +79,7 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") }) .middleware(CurrentUser.Authorization) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Get OAuth Authorization URL", description: "Returns the OAuth authorization URL for the provider. The frontend should redirect the user to this URL. Sets a session cookie to preserve context for the callback.", @@ -109,7 +109,7 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") error: [InvalidOAuthStateError, UnsupportedProviderError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "OAuth Callback", description: "Handle OAuth callback from integration provider", summary: "Process OAuth callback", @@ -132,7 +132,7 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") }) .middleware(CurrentUser.Authorization) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Get Connection Status", description: "Check the connection status for a provider", summary: "Get integration status", @@ -153,7 +153,7 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") }) .middleware(CurrentUser.Authorization) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Connect via API Key", description: "Connect an integration using an API key/token instead of OAuth. Validates the credentials against the provider and stores the connection.", @@ -176,7 +176,7 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") error: [IntegrationNotConnectedError, UnsupportedProviderError, UnauthorizedError, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Disconnect Integration", description: "Disconnect an integration and revoke tokens", summary: "Disconnect provider", diff --git a/packages/domain/src/http/presence.ts b/packages/domain/src/http/presence.ts index c50c2bcf0..5b417c1c5 100644 --- a/packages/domain/src/http/presence.ts +++ b/packages/domain/src/http/presence.ts @@ -22,7 +22,7 @@ export class PresencePublicGroup extends HttpApiGroup.make("presencePublic") error: InternalServerError, }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "Mark User Offline", description: "Mark a user as offline when they close their tab (no auth required)", summary: "Mark offline", diff --git a/packages/domain/src/http/webhooks.ts b/packages/domain/src/http/webhooks.ts index ef229a346..bc72fd5fc 100644 --- a/packages/domain/src/http/webhooks.ts +++ b/packages/domain/src/http/webhooks.ts @@ -50,7 +50,7 @@ export class WebhookGroup extends HttpApiGroup.make("webhooks") error: [InvalidWebhookSignature, InternalServerError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "WorkOS Webhook", description: "Receive and process WorkOS webhook events", summary: "Process WorkOS webhook events", @@ -65,7 +65,7 @@ export class WebhookGroup extends HttpApiGroup.make("webhooks") error: [InvalidGitHubWebhookSignature, InternalServerError, WorkflowInitializationError], }) .annotateMerge( - OpenApi.annotate({ + OpenApi.annotations({ title: "GitHub App Webhook", description: "Receive and process GitHub App webhook events", summary: "Process GitHub App webhook events", diff --git a/packages/domain/src/models/theme-model.ts b/packages/domain/src/models/theme-model.ts index 18415cc2f..cc607823f 100644 --- a/packages/domain/src/models/theme-model.ts +++ b/packages/domain/src/models/theme-model.ts @@ -96,16 +96,14 @@ export type DisplayMode = Schema.Schema.Type * User theme settings stored in the database. * Using mutable() to ensure compatibility with TanStack DB collections. */ -export const UserThemeSettings = Schema.mutable( - Schema.Struct({ - /** Active preset ID ("default", "ocean", etc.) or custom preset ID */ - activePresetId: Schema.NullOr(Schema.String), - /** Custom theme when not using a preset */ - customTheme: Schema.NullOr(Schema.mutable(ThemeCustomization)), - /** User's saved custom presets */ - savedPresets: Schema.optional(Schema.mutable(Schema.Array(Schema.mutable(ThemePreset)))), - /** Display mode preference */ - mode: DisplayMode, - }), -) +export const UserThemeSettings = Schema.Struct({ + /** Active preset ID ("default", "ocean", etc.) or custom preset ID */ + activePresetId: Schema.NullOr(Schema.String), + /** Custom theme when not using a preset */ + customTheme: Schema.NullOr(ThemeCustomization), + /** User's saved custom presets */ + savedPresets: Schema.optional(Schema.mutable(Schema.Array(ThemePreset))), + /** Display mode preference */ + mode: DisplayMode, +}) export type UserThemeSettings = Schema.Schema.Type diff --git a/packages/domain/src/models/utils.ts b/packages/domain/src/models/utils.ts index 2d1981d8a..242144f40 100644 --- a/packages/domain/src/models/utils.ts +++ b/packages/domain/src/models/utils.ts @@ -2,7 +2,9 @@ import * as VariantSchema from "effect/unstable/schema/VariantSchema" import type { Brand } from "effect/Brand" import * as DateTime from "effect/DateTime" import * as Effect from "effect/Effect" +import * as Option from "effect/Option" import * as Schema from "effect/Schema" +import * as SchemaGetter from "effect/SchemaGetter" import * as SchemaIssue from "effect/SchemaIssue" const { Class, Field, FieldExcept, FieldOnly, Struct, Union, extract, fieldEvolve } = @@ -189,15 +191,15 @@ export interface Date extends Schema.decodeTo< /** A DateTime.Utc serialized as ISO date string (YYYY-MM-DD). */ export const Date: Date = Schema.String.pipe( - Schema.decode({ - decode: (s: string) => { + Schema.decodeTo(Schema.DateTimeUtc, { + decode: SchemaGetter.transformOrFail((s: string) => { const opt = DateTime.make(s) if (opt._tag === "Some") { return Effect.succeed(DateTime.removeTime(opt.value)) } - return Effect.fail("Invalid date format") - }, - encode: (dt: DateTime.Utc) => Effect.succeed(DateTime.formatIsoDate(dt)), + return Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: "Invalid date format" })) + }), + encode: SchemaGetter.transform((dt: DateTime.Utc) => DateTime.formatIsoDate(dt)), }), ) as any diff --git a/packages/domain/src/rpc/scope-injection-middleware.ts b/packages/domain/src/rpc/scope-injection-middleware.ts index d6c9a2fa5..8c7106292 100644 --- a/packages/domain/src/rpc/scope-injection-middleware.ts +++ b/packages/domain/src/rpc/scope-injection-middleware.ts @@ -9,5 +9,4 @@ import { RpcMiddleware } from "effect/unstable/rpc" */ export class ScopeInjectionMiddleware extends RpcMiddleware.Service()( "ScopeInjectionMiddleware", - { wrap: true }, ) {} diff --git a/packages/domain/src/scopes/current-bot-scopes.ts b/packages/domain/src/scopes/current-bot-scopes.ts index 096e1f1cb..dd5742480 100644 --- a/packages/domain/src/scopes/current-bot-scopes.ts +++ b/packages/domain/src/scopes/current-bot-scopes.ts @@ -13,5 +13,4 @@ import type { ApiScope } from "./api-scope" */ export class CurrentBotScopes extends ServiceMap.Service>>()( "CurrentBotScopes", - { defaultValue: Option.none() }, ) {} diff --git a/packages/domain/src/scopes/current-rpc-scopes.ts b/packages/domain/src/scopes/current-rpc-scopes.ts index 9161453e3..b70c0f482 100644 --- a/packages/domain/src/scopes/current-rpc-scopes.ts +++ b/packages/domain/src/scopes/current-rpc-scopes.ts @@ -11,5 +11,4 @@ import type { ApiScope } from "./api-scope" */ export class CurrentRpcScopes extends ServiceMap.Service>()( "CurrentRpcScopes", - { defaultValue: [] as ReadonlyArray }, ) {} diff --git a/packages/domain/src/scopes/scope-map.ts b/packages/domain/src/scopes/scope-map.ts index 0e5f21b53..3603072f5 100644 --- a/packages/domain/src/scopes/scope-map.ts +++ b/packages/domain/src/scopes/scope-map.ts @@ -18,7 +18,7 @@ export const scopeMapFromRpcGroup = ( ): ScopeMap => { const map: Record> = {} for (const [tag, rpc] of requests) { - const scopes = ServiceMap.get(rpc.annotations, RequiredScopes) as ReadonlyArray | undefined + const scopes = ServiceMap.get(rpc.annotations as any, RequiredScopes) as ReadonlyArray | undefined if (scopes) { map[tag] = scopes } diff --git a/packages/domain/src/scopes/validate-scopes.ts b/packages/domain/src/scopes/validate-scopes.ts index e5e484647..df18d4632 100644 --- a/packages/domain/src/scopes/validate-scopes.ts +++ b/packages/domain/src/scopes/validate-scopes.ts @@ -11,7 +11,7 @@ export const validateRpcGroupScopes = ( ): { valid: boolean; missing: string[] } => { const missing: string[] = [] for (const [tag, rpc] of requests) { - const scopes = ServiceMap.get(rpc.annotations, RequiredScopes) as ReadonlyArray | undefined + const scopes = ServiceMap.get(rpc.annotations as any, RequiredScopes) as ReadonlyArray | undefined if (!scopes) { missing.push(`${groupName}.${tag}`) } diff --git a/packages/integrations/src/craft/api-client.ts b/packages/integrations/src/craft/api-client.ts index ffbd8793d..fa966a1cd 100644 --- a/packages/integrations/src/craft/api-client.ts +++ b/packages/integrations/src/craft/api-client.ts @@ -191,7 +191,7 @@ const normalizeCraftItemsResponse = (raw: unknown): unknown[] => { * Retry schedule for transient Craft API errors. * Retries up to 3 times with exponential backoff (100ms, 200ms, 400ms) */ -const makeRetrySchedule = Schedule.exponential("100 millis").pipe(Schedule.intersect(Schedule.recurs(3))) +const makeRetrySchedule = Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3))) /** * Check if an error is retryable (rate limit or server error) diff --git a/packages/integrations/src/github/api-client.ts b/packages/integrations/src/github/api-client.ts index 4c78f1cf6..f8fc3f83a 100644 --- a/packages/integrations/src/github/api-client.ts +++ b/packages/integrations/src/github/api-client.ts @@ -269,7 +269,7 @@ const DEFAULT_TIMEOUT = Duration.seconds(30) * Retries up to 3 times with exponential backoff (100ms, 200ms, 400ms) * only for rate limits (429) and server errors (5xx). */ -const makeRetrySchedule = Schedule.exponential("100 millis").pipe(Schedule.intersect(Schedule.recurs(3))) +const makeRetrySchedule = Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3))) /** * Check if an error is retryable (rate limit or server error) diff --git a/packages/integrations/src/github/payloads.ts b/packages/integrations/src/github/payloads.ts index cfa65f150..11fb36bc8 100644 --- a/packages/integrations/src/github/payloads.ts +++ b/packages/integrations/src/github/payloads.ts @@ -207,7 +207,7 @@ export type GitHubWebhookPayload = /** * GitHub event types supported by the integration. */ -export const GitHubEventType = Schema.Literal( +export const GitHubEventType = Schema.Literals([ "push", "pull_request", "issues", @@ -216,7 +216,7 @@ export const GitHubEventType = Schema.Literal( "workflow_run", "star", "milestone", -) +]) export type GitHubEventType = Schema.Schema.Type export const GitHubEventTypes = Schema.Array(GitHubEventType) diff --git a/packages/integrations/src/linear/api-client.ts b/packages/integrations/src/linear/api-client.ts index d1b227189..b53e2b543 100644 --- a/packages/integrations/src/linear/api-client.ts +++ b/packages/integrations/src/linear/api-client.ts @@ -380,7 +380,7 @@ const parseLinearErrorMessage = (errorMessage: string): string => { * Retry schedule for transient Linear API errors. * Retries up to 3 times with exponential backoff (100ms, 200ms, 400ms) */ -const makeRetrySchedule = Schedule.exponential("100 millis").pipe(Schedule.intersect(Schedule.recurs(3))) +const makeRetrySchedule = Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3))) /** * Check if an error is retryable (rate limit or server error) diff --git a/packages/schema/src/avatar-url.ts b/packages/schema/src/avatar-url.ts index 08656678d..dce3c552c 100644 --- a/packages/schema/src/avatar-url.ts +++ b/packages/schema/src/avatar-url.ts @@ -1,5 +1,5 @@ import { HttpClient } from "effect/unstable/http" -import { Duration, Effect, Option, Schema } from "effect" +import { Duration, Effect, Option, Schema, SchemaGetter } from "effect" export class InvalidAvatarUrlError extends Schema.TaggedErrorClass()( "InvalidAvatarUrlError", @@ -20,66 +20,71 @@ export const validateImageUrl = Effect.fn("validateImageUrl")(function* (url: st .pipe(Effect.scoped, Effect.timeout(Duration.seconds(5))) .pipe( Effect.catchTag( - "TimeoutException", + "TimeoutError", () => - new InvalidAvatarUrlError({ - message: "Avatar URL took too long to respond", - url, - }), + Effect.fail( + new InvalidAvatarUrlError({ + message: "Avatar URL took too long to respond", + url, + }), + ), ), Effect.catchTag( - "RequestError", - () => - new InvalidAvatarUrlError({ - message: "Avatar URL could not be reached", - url, - }), - ), - Effect.catchTag( - "ResponseError", + "HttpClientError", (e) => - new InvalidAvatarUrlError({ - message: `Avatar URL returned ${e.response.status} error`, - url, - }), + Effect.fail( + new InvalidAvatarUrlError({ + message: `Avatar URL request failed: ${e.message}`, + url, + }), + ), ), ) if (response.status >= 400) { - return yield* new InvalidAvatarUrlError({ - message: `Avatar URL returned ${response.status} error`, - url, - }) + return yield* Effect.fail( + new InvalidAvatarUrlError({ + message: `Avatar URL returned ${response.status} error`, + url, + }), + ) } - const contentType = Option.fromNullable(response.headers["content-type"]) + const contentType = Option.fromNullOr(response.headers["content-type"]) const isImage = Option.match(contentType, { onNone: () => false, onSome: (ct) => ct.startsWith("image/"), }) if (!isImage) { - return yield* new InvalidAvatarUrlError({ - message: "Avatar URL must point to an image", - url, - }) + return yield* Effect.fail( + new InvalidAvatarUrlError({ + message: "Avatar URL must point to an image", + url, + }), + ) } }) -export const AvatarUrl = Schema.String.pipe( - Schema.pattern(/^https?:\/\/.+/i, { - message: () => "Avatar URL must be a valid URL", - }), - Schema.isMaxLength(2048), - Schema.filterEffect((url) => - validateImageUrl(url).pipe( - Effect.map(() => true), - Effect.catch((e) => Effect.succeed(e.message)), - ), - ), -).annotate({ - description: "A validated URL to an avatar image", - title: "Avatar URL", -}) +export const AvatarUrl = Schema.String + .check(Schema.isPattern(/^https?:\/\/.+/i, { + message: "Avatar URL must be a valid URL", + })) + .check(Schema.isMaxLength(2048)) + .pipe( + Schema.decode({ + decode: SchemaGetter.checkEffect((url: string) => + validateImageUrl(url).pipe( + Effect.map(() => true as const), + Effect.catch((e: InvalidAvatarUrlError) => Effect.succeed(e.message)), + ), + ), + encode: SchemaGetter.passthrough(), + }), + ) + .annotate({ + description: "A validated URL to an avatar image", + title: "Avatar URL", + }) export type AvatarUrl = Schema.Schema.Type diff --git a/packages/schema/src/workos.ts b/packages/schema/src/workos.ts index fa370d37f..5fee46513 100644 --- a/packages/schema/src/workos.ts +++ b/packages/schema/src/workos.ts @@ -1,6 +1,6 @@ import { Schema } from "effect" -export const WorkOSUserId = Schema.NonEmptyTrimmedString.pipe( +export const WorkOSUserId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand("@HazelChat/WorkOSUserId"), ).annotate({ description: "A WorkOS user identifier", @@ -8,7 +8,7 @@ export const WorkOSUserId = Schema.NonEmptyTrimmedString.pipe( }) export type WorkOSUserId = Schema.Schema.Type -export const WorkOSOrganizationId = Schema.NonEmptyTrimmedString.pipe( +export const WorkOSOrganizationId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand("@HazelChat/WorkOSOrganizationId"), ).annotate({ description: "A WorkOS organization identifier", @@ -16,7 +16,7 @@ export const WorkOSOrganizationId = Schema.NonEmptyTrimmedString.pipe( }) export type WorkOSOrganizationId = Schema.Schema.Type -export const WorkOSSessionId = Schema.NonEmptyTrimmedString.pipe( +export const WorkOSSessionId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand("@HazelChat/WorkOSSessionId"), ).annotate({ description: "A WorkOS session identifier", @@ -24,7 +24,7 @@ export const WorkOSSessionId = Schema.NonEmptyTrimmedString.pipe( }) export type WorkOSSessionId = Schema.Schema.Type -export const WorkOSInvitationId = Schema.NonEmptyTrimmedString.pipe( +export const WorkOSInvitationId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand("@HazelChat/WorkOSInvitationId"), ).annotate({ description: "A WorkOS invitation identifier", @@ -32,7 +32,7 @@ export const WorkOSInvitationId = Schema.NonEmptyTrimmedString.pipe( }) export type WorkOSInvitationId = Schema.Schema.Type -export const WorkOSClientId = Schema.NonEmptyTrimmedString.pipe( +export const WorkOSClientId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand("@HazelChat/WorkOSClientId"), ).annotate({ description: "A WorkOS client identifier", diff --git a/packages/setup/src/commands/bots.ts b/packages/setup/src/commands/bots.ts index 4a015c3b2..627813bee 100644 --- a/packages/setup/src/commands/bots.ts +++ b/packages/setup/src/commands/bots.ts @@ -1,4 +1,4 @@ -import { Command, Options, Prompt } from "effect/unstable/cli" +import { Command, Flag, Prompt } from "effect/unstable/cli" import { Database, schema, isNull } from "@hazel/db" import type { BotId, BotInstallationId, OrganizationId, OrganizationMemberId, UserId } from "@hazel/schema" import { Console, Effect, Option, Redacted } from "effect" @@ -6,11 +6,11 @@ import { randomUUID } from "crypto" import pc from "picocolors" // CLI Options -const nameOption = Options.text("name").pipe(Options.withDescription("Bot name"), Options.optional) +const nameOption = Flag.string("name").pipe(Flag.withDescription("Bot name"), Flag.optional) -const orgOption = Options.text("org").pipe( - Options.withDescription("Organization ID to install bot in"), - Options.optional, +const orgOption = Flag.string("org").pipe( + Flag.withDescription("Organization ID to install bot in"), + Flag.optional, ) /** diff --git a/packages/setup/src/commands/certs.ts b/packages/setup/src/commands/certs.ts index 1a9ce4441..fa5631d8b 100644 --- a/packages/setup/src/commands/certs.ts +++ b/packages/setup/src/commands/certs.ts @@ -3,61 +3,62 @@ import { Console, Effect } from "effect" import pc from "picocolors" import { CertManager } from "../services/cert-manager.ts" -export const certsCommand = Command.make("certs", {}, () => - Effect.gen(function* () { - yield* Console.log(`\n${pc.bold("HTTPS Certificate Setup")}\n`) - - const certs = yield* CertManager - - // Check if certs already exist - const exists = yield* certs.certsExist() - if (exists) { - yield* Console.log(pc.green("\u2713") + " Certificates already exist at:") - yield* Console.log(pc.dim(` ${certs.certPath}`)) - yield* Console.log(pc.dim(` ${certs.keyPath}`)) - - const regenerate = yield* Prompt.confirm({ - message: "Regenerate certificates?", - initial: false, - }) - if (!regenerate) return - } +/** Shared certs setup logic, callable from both the certs subcommand and the main setup command. */ +export const certsSetupEffect = Effect.gen(function* () { + yield* Console.log(`\n${pc.bold("HTTPS Certificate Setup")}\n`) + + const certs = yield* CertManager + + // Check if certs already exist + const exists = yield* certs.certsExist() + if (exists) { + yield* Console.log(pc.green("\u2713") + " Certificates already exist at:") + yield* Console.log(pc.dim(` ${certs.certPath}`)) + yield* Console.log(pc.dim(` ${certs.keyPath}`)) - // Check mkcert installation - yield* Console.log(pc.cyan("\u2500\u2500\u2500 Checking mkcert \u2500\u2500\u2500")) - const hasMkcert = yield* certs.checkMkcert() - - if (!hasMkcert) { - yield* Console.log(pc.yellow("mkcert not found.")) - const install = yield* Prompt.confirm({ - message: "Install mkcert via Homebrew?", - initial: true, - }) - if (!install) { - yield* Console.log(pc.dim("Install manually: brew install mkcert")) - return - } - yield* certs.installMkcert() - yield* Console.log(pc.green("\u2713") + " mkcert installed") - } else { - yield* Console.log(pc.green("\u2713") + " mkcert found") + const regenerate = yield* Prompt.confirm({ + message: "Regenerate certificates?", + initial: false, + }) + if (!regenerate) return + } + + // Check mkcert installation + yield* Console.log(pc.cyan("\u2500\u2500\u2500 Checking mkcert \u2500\u2500\u2500")) + const hasMkcert = yield* certs.checkMkcert() + + if (!hasMkcert) { + yield* Console.log(pc.yellow("mkcert not found.")) + const install = yield* Prompt.confirm({ + message: "Install mkcert via Homebrew?", + initial: true, + }) + if (!install) { + yield* Console.log(pc.dim("Install manually: brew install mkcert")) + return } + yield* certs.installMkcert() + yield* Console.log(pc.green("\u2713") + " mkcert installed") + } else { + yield* Console.log(pc.green("\u2713") + " mkcert found") + } - // Install CA - yield* Console.log(pc.cyan("\n\u2500\u2500\u2500 Installing Local CA \u2500\u2500\u2500")) - yield* Console.log(pc.dim("This may require your password...")) - yield* certs.installCA() - yield* Console.log(pc.green("\u2713") + " Local CA installed") + // Install CA + yield* Console.log(pc.cyan("\n\u2500\u2500\u2500 Installing Local CA \u2500\u2500\u2500")) + yield* Console.log(pc.dim("This may require your password...")) + yield* certs.installCA() + yield* Console.log(pc.green("\u2713") + " Local CA installed") - // Generate certificates - yield* Console.log(pc.cyan("\n\u2500\u2500\u2500 Generating Certificates \u2500\u2500\u2500")) - yield* certs.generateCerts() - yield* Console.log(pc.green("\u2713") + " Certificates generated") + // Generate certificates + yield* Console.log(pc.cyan("\n\u2500\u2500\u2500 Generating Certificates \u2500\u2500\u2500")) + yield* certs.generateCerts() + yield* Console.log(pc.green("\u2713") + " Certificates generated") - yield* Console.log(pc.green("\n\u2705 HTTPS setup complete!")) - yield* Console.log(pc.bold("\nCertificates at:")) - yield* Console.log(pc.dim(` ${certs.certPath}`)) - yield* Console.log(pc.dim(` ${certs.keyPath}`)) - yield* Console.log(pc.dim("\nRestart dev servers to use HTTPS.")) - }), -) + yield* Console.log(pc.green("\n\u2705 HTTPS setup complete!")) + yield* Console.log(pc.bold("\nCertificates at:")) + yield* Console.log(pc.dim(` ${certs.certPath}`)) + yield* Console.log(pc.dim(` ${certs.keyPath}`)) + yield* Console.log(pc.dim("\nRestart dev servers to use HTTPS.")) +}) + +export const certsCommand = Command.make("certs", {}, () => certsSetupEffect) diff --git a/packages/setup/src/commands/env.ts b/packages/setup/src/commands/env.ts index 9f287c20a..93aeadcb8 100644 --- a/packages/setup/src/commands/env.ts +++ b/packages/setup/src/commands/env.ts @@ -1,4 +1,4 @@ -import { Command, Options, Prompt } from "effect/unstable/cli" +import { Command, Flag, Prompt } from "effect/unstable/cli" import { Console, Effect, Redacted } from "effect" import pc from "picocolors" import { SecretGenerator } from "../services/secrets.ts" @@ -8,21 +8,21 @@ import { ENV_TEMPLATES, extractExistingConfig, maskSecret, type Config, type S3C import { promptWithExisting, getExistingValue } from "../prompts.ts" // CLI Options -export const skipValidation = Options.boolean("skip-validation").pipe( - Options.withDescription("Skip credential validation (API calls)"), - Options.withDefault(false), +export const skipValidation = Flag.boolean("skip-validation").pipe( + Flag.withDescription("Skip credential validation (API calls)"), + Flag.withDefault(false), ) -export const force = Options.boolean("force").pipe( - Options.withAlias("f"), - Options.withDescription("Overwrite existing .env files without prompting"), - Options.withDefault(false), +export const force = Flag.boolean("force").pipe( + Flag.withAlias("f"), + Flag.withDescription("Overwrite existing .env files without prompting"), + Flag.withDefault(false), ) -export const dryRun = Options.boolean("dry-run").pipe( - Options.withAlias("n"), - Options.withDescription("Show what would be done without writing files"), - Options.withDefault(false), +export const dryRun = Flag.boolean("dry-run").pipe( + Flag.withAlias("n"), + Flag.withDescription("Show what would be done without writing files"), + Flag.withDefault(false), ) export const envCommand = Command.make( @@ -72,9 +72,9 @@ export const envCommand = Command.make( const validator = yield* CredentialValidator const dbResult = yield* validator .validateDatabase("postgresql://user:password@localhost:5432/app") - .pipe(Effect.either) + .pipe(Effect.result) - if (dbResult._tag === "Left") { + if (dbResult._tag === "Failure") { yield* Console.log( pc.yellow("\u26A0\uFE0F Database not reachable.") + ` Run ${pc.cyan("`docker compose up -d`")} first.`, @@ -127,10 +127,10 @@ export const envCommand = Command.make( const validator = yield* CredentialValidator const result = yield* validator .validateWorkOS(workosApiKey, workosClientId) - .pipe(Effect.either) + .pipe(Effect.result) - if (result._tag === "Left") { - yield* Console.log(pc.red(`\u274C WorkOS validation failed: ${result.left.message}`)) + if (result._tag === "Failure") { + yield* Console.log(pc.red(`\u274C WorkOS validation failed: ${result.failure.message}`)) yield* Console.log(pc.dim("Please check your credentials and try again.")) return } diff --git a/packages/setup/src/commands/setup.ts b/packages/setup/src/commands/setup.ts index 7a62b4582..1a3a4ef79 100644 --- a/packages/setup/src/commands/setup.ts +++ b/packages/setup/src/commands/setup.ts @@ -1,4 +1,4 @@ -import { Command, Options, Prompt } from "effect/unstable/cli" +import { Command, Flag, Prompt } from "effect/unstable/cli" import { Console, Effect, Redacted } from "effect" import pc from "picocolors" import { SecretGenerator } from "../services/secrets.ts" @@ -13,29 +13,29 @@ import { type Config, } from "../templates.ts" import { promptWithExisting, getExistingValue } from "../prompts.ts" -import { certsCommand } from "./certs.ts" +import { certsCommand, certsSetupEffect } from "./certs.ts" // CLI Options -const skipValidation = Options.boolean("skip-validation").pipe( - Options.withDescription("Skip credential validation (API calls)"), - Options.withDefault(false), +const skipValidation = Flag.boolean("skip-validation").pipe( + Flag.withDescription("Skip credential validation (API calls)"), + Flag.withDefault(false), ) -const force = Options.boolean("force").pipe( - Options.withAlias("f"), - Options.withDescription("Overwrite existing .env files without prompting"), - Options.withDefault(false), +const force = Flag.boolean("force").pipe( + Flag.withAlias("f"), + Flag.withDescription("Overwrite existing .env files without prompting"), + Flag.withDefault(false), ) -const dryRun = Options.boolean("dry-run").pipe( - Options.withAlias("n"), - Options.withDescription("Show what would be done without writing files"), - Options.withDefault(false), +const dryRun = Flag.boolean("dry-run").pipe( + Flag.withAlias("n"), + Flag.withDescription("Show what would be done without writing files"), + Flag.withDefault(false), ) -const skipDoctor = Options.boolean("skip-doctor").pipe( - Options.withDescription("Skip environment checks"), - Options.withDefault(false), +const skipDoctor = Flag.boolean("skip-doctor").pipe( + Flag.withDescription("Skip environment checks"), + Flag.withDefault(false), ) export const setupCommand = Command.make( @@ -46,7 +46,7 @@ export const setupCommand = Command.make( yield* Console.log(`\n${pc.bold("\u{1F33F} Hazel Local Development Setup")}\n`) // Run the certs setup - yield* certsCommand.handler({}) + yield* certsSetupEffect // Start Docker Compose after that yield* Console.log(pc.cyan("\u2500\u2500\u2500 Starting Docker Compose \u2500\u2500\u2500")) @@ -187,9 +187,9 @@ export const setupCommand = Command.make( const validator = yield* CredentialValidator const dbResult = yield* validator .validateDatabase("postgresql://user:password@localhost:5432/app") - .pipe(Effect.either) + .pipe(Effect.result) - if (dbResult._tag === "Left") { + if (dbResult._tag === "Failure") { yield* Console.log( pc.yellow("\u26A0\uFE0F Database not reachable.") + ` Run ${pc.cyan("`docker compose up -d`")} first.`, @@ -242,10 +242,10 @@ export const setupCommand = Command.make( const validator = yield* CredentialValidator const result = yield* validator .validateWorkOS(workosApiKey, workosClientId) - .pipe(Effect.either) + .pipe(Effect.result) - if (result._tag === "Left") { - yield* Console.log(pc.red(`\u274C WorkOS validation failed: ${result.left.message}`)) + if (result._tag === "Failure") { + yield* Console.log(pc.red(`\u274C WorkOS validation failed: ${result.failure.message}`)) yield* Console.log(pc.dim("Please check your credentials and try again.")) return } diff --git a/packages/setup/src/index.ts b/packages/setup/src/index.ts index ec8e1f9ef..1f8079e3c 100644 --- a/packages/setup/src/index.ts +++ b/packages/setup/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun import { Command } from "effect/unstable/cli" -import { BunContext, BunRuntime } from "@effect/platform-bun" +import { BunServices, BunRuntime } from "@effect/platform-bun" import { Effect, Layer } from "effect" import { existsSync, readFileSync } from "fs" import { resolve } from "path" @@ -41,16 +41,6 @@ const loadDatabaseUrl = () => { loadDatabaseUrl() -// Root command with subcommands -const rootCommand = setupCommand.pipe( - Command.withSubcommands([doctorCommand, envCommand, certsCommand, botsCommand]), -) - -const cli = Command.run(rootCommand, { - name: "hazel-setup", - version: "0.0.1", -}) - const ServicesLive = Layer.mergeAll( SecretGenerator.layer, CredentialValidator.layer, @@ -59,4 +49,15 @@ const ServicesLive = Layer.mergeAll( CertManager.layer, ) -cli(process.argv).pipe(Effect.provide(ServicesLive), Effect.provide(BunContext.layer), BunRuntime.runMain) +// Root command with subcommands, run via v4 CLI pattern +// Note: `any` in R position is due to @hazel/db Database types not yet migrated to v4 +const cli = setupCommand.pipe( + Command.withSubcommands([doctorCommand, envCommand, certsCommand, botsCommand]), + Command.run({ + version: "0.0.1", + }), + Effect.provide(ServicesLive), + Effect.provide(BunServices.layer), +) + +BunRuntime.runMain(cli as Effect.Effect) diff --git a/packages/setup/src/services/cert-manager.ts b/packages/setup/src/services/cert-manager.ts index d883a7c27..a0dc49cc4 100644 --- a/packages/setup/src/services/cert-manager.ts +++ b/packages/setup/src/services/cert-manager.ts @@ -1,4 +1,4 @@ -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" import { resolve } from "node:path" export interface CertPaths { diff --git a/packages/setup/src/services/doctor.ts b/packages/setup/src/services/doctor.ts index 7912815ec..f9b068694 100644 --- a/packages/setup/src/services/doctor.ts +++ b/packages/setup/src/services/doctor.ts @@ -1,4 +1,4 @@ -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" export interface CheckResult { name: string diff --git a/packages/setup/src/services/env-writer.ts b/packages/setup/src/services/env-writer.ts index 5a9359678..e13a1cb3e 100644 --- a/packages/setup/src/services/env-writer.ts +++ b/packages/setup/src/services/env-writer.ts @@ -1,4 +1,4 @@ -import { ServiceMap, Console, Effect } from "effect" +import { ServiceMap, Console, Effect, Layer } from "effect" import { dirname } from "node:path" import { mkdir } from "node:fs/promises" diff --git a/packages/setup/src/services/secrets.ts b/packages/setup/src/services/secrets.ts index a345f05eb..5c7843aab 100644 --- a/packages/setup/src/services/secrets.ts +++ b/packages/setup/src/services/secrets.ts @@ -1,4 +1,4 @@ -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" export class SecretGenerator extends ServiceMap.Service()("SecretGenerator", { make: Effect.succeed({ diff --git a/packages/setup/src/services/validators.ts b/packages/setup/src/services/validators.ts index 63a646cf6..786ef6399 100644 --- a/packages/setup/src/services/validators.ts +++ b/packages/setup/src/services/validators.ts @@ -1,4 +1,4 @@ -import { ServiceMap, Data, Effect } from "effect" +import { ServiceMap, Data, Effect, Layer } from "effect" import { WorkOS } from "@workos-inc/node" import { SQL } from "bun" From b7ec9cc41cd3d8ad8065d21fcddc5ee58a7ebf14 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 08:46:09 +0100 Subject: [PATCH 10/34] fix --- .../backend-core/src/repositories/bot-repo.ts | 2 +- .../src/repositories/channel-member-repo.ts | 2 +- .../src/repositories/channel-repo.ts | 2 +- .../src/repositories/channel-webhook-repo.ts | 2 +- .../chat-sync-channel-link-repo.ts | 2 +- .../repositories/chat-sync-connection-repo.ts | 2 +- .../chat-sync-event-receipt-repo.ts | 2 +- .../chat-sync-message-link-repo.ts | 2 +- .../connect-conversation-channel-repo.ts | 2 +- .../repositories/connect-conversation-repo.ts | 2 +- .../src/repositories/connect-invite-repo.ts | 2 +- .../repositories/connect-participant-repo.ts | 2 +- .../src/repositories/custom-emoji-repo.ts | 2 +- .../repositories/github-subscription-repo.ts | 2 +- .../integration-connection-repo.ts | 2 +- .../repositories/integration-token-repo.ts | 2 +- .../src/repositories/invitation-repo.ts | 2 +- .../src/repositories/message-outbox-repo.ts | 2 +- .../src/repositories/message-reaction-repo.ts | 2 +- .../src/repositories/message-repo.ts | 2 +- .../repositories/organization-member-repo.ts | 2 +- .../src/repositories/rss-subscription-repo.ts | 2 +- .../src/repositories/typing-indicator-repo.ts | 2 +- .../src/repositories/user-repo.ts | 2 +- packages/backend-core/src/services/workos.ts | 2 +- packages/db/src/services/database.ts | 8 +- packages/db/src/services/drizzle-effect.ts | 81 ++++++++----------- packages/db/src/services/model-repository.ts | 14 ++-- packages/db/src/services/model.ts | 10 +-- 29 files changed, 73 insertions(+), 90 deletions(-) diff --git a/packages/backend-core/src/repositories/bot-repo.ts b/packages/backend-core/src/repositories/bot-repo.ts index 33b81f0f7..5c64cd98a 100644 --- a/packages/backend-core/src/repositories/bot-repo.ts +++ b/packages/backend-core/src/repositories/bot-repo.ts @@ -14,7 +14,7 @@ import { import type { BotId, UserId } from "@hazel/schema" import { Bot } from "@hazel/domain/models" -import { ServiceMap, Effect, Option, type Schema } from "effect" +import { ServiceMap, Effect, Layer, Option, type Schema } from "effect" export class BotRepo extends ServiceMap.Service()("BotRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/channel-member-repo.ts b/packages/backend-core/src/repositories/channel-member-repo.ts index 53777f1b4..978de3e98 100644 --- a/packages/backend-core/src/repositories/channel-member-repo.ts +++ b/packages/backend-core/src/repositories/channel-member-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, inArray, isNull, ModelRepository, schema, sql, type import type { ChannelId, OrganizationId, UserId } from "@hazel/schema" import { ChannelMember } from "@hazel/domain/models" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" export class ChannelMemberRepo extends ServiceMap.Service()("ChannelMemberRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/channel-repo.ts b/packages/backend-core/src/repositories/channel-repo.ts index 7d5f03593..3acab47a5 100644 --- a/packages/backend-core/src/repositories/channel-repo.ts +++ b/packages/backend-core/src/repositories/channel-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import type { OrganizationId } from "@hazel/schema" import { Channel } from "@hazel/domain/models" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" export class ChannelRepo extends ServiceMap.Service()("ChannelRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/channel-webhook-repo.ts b/packages/backend-core/src/repositories/channel-webhook-repo.ts index 1560c499f..555e552f7 100644 --- a/packages/backend-core/src/repositories/channel-webhook-repo.ts +++ b/packages/backend-core/src/repositories/channel-webhook-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import type { ChannelId, ChannelWebhookId, OrganizationId } from "@hazel/schema" import { ChannelWebhook } from "@hazel/domain/models" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" export class ChannelWebhookRepo extends ServiceMap.Service()("ChannelWebhookRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts b/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts index 4907b2663..36492f16f 100644 --- a/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts @@ -7,7 +7,7 @@ import { ServiceMap, Effect, Option, Schema } from "effect" export class ChatSyncChannelLinkRepo extends ServiceMap.Service()( "ChatSyncChannelLinkRepo", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.chatSyncChannelLinksTable, ChatSyncChannelLink.Model, diff --git a/packages/backend-core/src/repositories/chat-sync-connection-repo.ts b/packages/backend-core/src/repositories/chat-sync-connection-repo.ts index 79cf73829..f2c247feb 100644 --- a/packages/backend-core/src/repositories/chat-sync-connection-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-connection-repo.ts @@ -7,7 +7,7 @@ import { ServiceMap, Effect, Option } from "effect" export class ChatSyncConnectionRepo extends ServiceMap.Service()( "ChatSyncConnectionRepo", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.chatSyncConnectionsTable, ChatSyncConnection.Model, diff --git a/packages/backend-core/src/repositories/chat-sync-event-receipt-repo.ts b/packages/backend-core/src/repositories/chat-sync-event-receipt-repo.ts index 881a7d428..57f3601c0 100644 --- a/packages/backend-core/src/repositories/chat-sync-event-receipt-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-event-receipt-repo.ts @@ -7,7 +7,7 @@ import { ServiceMap, Effect, Option } from "effect" export class ChatSyncEventReceiptRepo extends ServiceMap.Service()( "ChatSyncEventReceiptRepo", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.chatSyncEventReceiptsTable, ChatSyncEventReceipt.Model, diff --git a/packages/backend-core/src/repositories/chat-sync-message-link-repo.ts b/packages/backend-core/src/repositories/chat-sync-message-link-repo.ts index db35e33d1..0da040a94 100644 --- a/packages/backend-core/src/repositories/chat-sync-message-link-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-message-link-repo.ts @@ -13,7 +13,7 @@ import { ServiceMap, Effect, Option, Schema } from "effect" export class ChatSyncMessageLinkRepo extends ServiceMap.Service()( "ChatSyncMessageLinkRepo", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.chatSyncMessageLinksTable, ChatSyncMessageLink.Model, diff --git a/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts b/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts index 407e3d6a0..360256616 100644 --- a/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts +++ b/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts @@ -6,7 +6,7 @@ import { ServiceMap, Effect, Option } from "effect" export class ConnectConversationChannelRepo extends ServiceMap.Service()( "ConnectConversationChannelRepo", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.connectConversationChannelsTable, ConnectConversationChannel.Model, diff --git a/packages/backend-core/src/repositories/connect-conversation-repo.ts b/packages/backend-core/src/repositories/connect-conversation-repo.ts index c9c11e946..d287e34be 100644 --- a/packages/backend-core/src/repositories/connect-conversation-repo.ts +++ b/packages/backend-core/src/repositories/connect-conversation-repo.ts @@ -6,7 +6,7 @@ import { ServiceMap, Effect, Option } from "effect" export class ConnectConversationRepo extends ServiceMap.Service()( "ConnectConversationRepo", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.connectConversationsTable, ConnectConversation.Model, diff --git a/packages/backend-core/src/repositories/connect-invite-repo.ts b/packages/backend-core/src/repositories/connect-invite-repo.ts index fbb9fcfc5..d6b748ad3 100644 --- a/packages/backend-core/src/repositories/connect-invite-repo.ts +++ b/packages/backend-core/src/repositories/connect-invite-repo.ts @@ -1,7 +1,7 @@ import { and, Database, eq, isNull, ModelRepository, or, schema, type TxFn } from "@hazel/db" import type { ConnectInviteId, OrganizationId } from "@hazel/schema" import { ConnectInvite } from "@hazel/domain/models" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" export class ConnectInviteRepo extends ServiceMap.Service()("ConnectInviteRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/connect-participant-repo.ts b/packages/backend-core/src/repositories/connect-participant-repo.ts index e9e614d78..254d80f52 100644 --- a/packages/backend-core/src/repositories/connect-participant-repo.ts +++ b/packages/backend-core/src/repositories/connect-participant-repo.ts @@ -6,7 +6,7 @@ import { ServiceMap, Effect, Option, type Schema as EffectSchema } from "effect" export class ConnectParticipantRepo extends ServiceMap.Service()( "ConnectParticipantRepo", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.connectParticipantsTable, ConnectParticipant.Model, diff --git a/packages/backend-core/src/repositories/custom-emoji-repo.ts b/packages/backend-core/src/repositories/custom-emoji-repo.ts index a1cde3590..cec9d6a72 100644 --- a/packages/backend-core/src/repositories/custom-emoji-repo.ts +++ b/packages/backend-core/src/repositories/custom-emoji-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, isNotNull, isNull, ModelRepository, schema } from "@ import type { CustomEmojiId, OrganizationId } from "@hazel/schema" import { CustomEmoji } from "@hazel/domain/models" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" export class CustomEmojiRepo extends ServiceMap.Service()("CustomEmojiRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/github-subscription-repo.ts b/packages/backend-core/src/repositories/github-subscription-repo.ts index 20fd6ec30..87e5b5173 100644 --- a/packages/backend-core/src/repositories/github-subscription-repo.ts +++ b/packages/backend-core/src/repositories/github-subscription-repo.ts @@ -7,7 +7,7 @@ import { ServiceMap, Effect, Option } from "effect" export class GitHubSubscriptionRepo extends ServiceMap.Service()( "GitHubSubscriptionRepo", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.githubSubscriptionsTable, GitHubSubscription.Model, diff --git a/packages/backend-core/src/repositories/integration-connection-repo.ts b/packages/backend-core/src/repositories/integration-connection-repo.ts index 1b7414996..b2a182420 100644 --- a/packages/backend-core/src/repositories/integration-connection-repo.ts +++ b/packages/backend-core/src/repositories/integration-connection-repo.ts @@ -7,7 +7,7 @@ import { ServiceMap, Effect, Option } from "effect" export class IntegrationConnectionRepo extends ServiceMap.Service()( "IntegrationConnectionRepo", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.integrationConnectionsTable, IntegrationConnection.Model, diff --git a/packages/backend-core/src/repositories/integration-token-repo.ts b/packages/backend-core/src/repositories/integration-token-repo.ts index c52ab055b..65dc29f9d 100644 --- a/packages/backend-core/src/repositories/integration-token-repo.ts +++ b/packages/backend-core/src/repositories/integration-token-repo.ts @@ -2,7 +2,7 @@ import { Database, eq, ModelRepository, schema, type TxFn } from "@hazel/db" import type { IntegrationConnectionId, IntegrationTokenId } from "@hazel/schema" import { IntegrationToken } from "@hazel/domain/models" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" export class IntegrationTokenRepo extends ServiceMap.Service()("IntegrationTokenRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/invitation-repo.ts b/packages/backend-core/src/repositories/invitation-repo.ts index eb448fc4f..2424ba3e5 100644 --- a/packages/backend-core/src/repositories/invitation-repo.ts +++ b/packages/backend-core/src/repositories/invitation-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, lte, ModelRepository, schema, type TxFn } from "@haz import type { InvitationId, OrganizationId, WorkOSInvitationId } from "@hazel/schema" import { Invitation } from "@hazel/domain/models" -import { ServiceMap, Effect, Option, type Schema } from "effect" +import { ServiceMap, Effect, Layer, Option, type Schema } from "effect" export class InvitationRepo extends ServiceMap.Service()("InvitationRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/message-outbox-repo.ts b/packages/backend-core/src/repositories/message-outbox-repo.ts index c91827526..f1be20410 100644 --- a/packages/backend-core/src/repositories/message-outbox-repo.ts +++ b/packages/backend-core/src/repositories/message-outbox-repo.ts @@ -1,7 +1,7 @@ import { Database, and, asc, eq, inArray, or, schema, sql } from "@hazel/db" import type { DatabaseError, TxFn } from "@hazel/db" import { ChannelId, MessageId, MessageOutboxEventId, MessageReactionId, UserId } from "@hazel/schema" -import { ServiceMap, Effect, Option, Schema } from "effect" +import { ServiceMap, Effect, Layer, Option, Schema } from "effect" export const MessageCreatedPayloadSchema = Schema.Struct({ messageId: MessageId, diff --git a/packages/backend-core/src/repositories/message-reaction-repo.ts b/packages/backend-core/src/repositories/message-reaction-repo.ts index 716f18456..90a08c79a 100644 --- a/packages/backend-core/src/repositories/message-reaction-repo.ts +++ b/packages/backend-core/src/repositories/message-reaction-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import type { ChannelId, ConnectConversationId, MessageId, UserId } from "@hazel/schema" import { MessageReaction } from "@hazel/domain/models" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" export class MessageReactionRepo extends ServiceMap.Service()("MessageReactionRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/message-repo.ts b/packages/backend-core/src/repositories/message-repo.ts index 07969ea73..dd1e0c4ff 100644 --- a/packages/backend-core/src/repositories/message-repo.ts +++ b/packages/backend-core/src/repositories/message-repo.ts @@ -16,7 +16,7 @@ import { import type { ChannelId, ConnectConversationId, MessageId, OrganizationId, UserId } from "@hazel/schema" import { Message } from "@hazel/domain/models" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" export interface ListByChannelParams { channelId: ChannelId diff --git a/packages/backend-core/src/repositories/organization-member-repo.ts b/packages/backend-core/src/repositories/organization-member-repo.ts index 5eb578cba..dae97fe01 100644 --- a/packages/backend-core/src/repositories/organization-member-repo.ts +++ b/packages/backend-core/src/repositories/organization-member-repo.ts @@ -7,7 +7,7 @@ import { ServiceMap, Effect, Option, type Schema } from "effect" export class OrganizationMemberRepo extends ServiceMap.Service()( "OrganizationMemberRepo", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const baseRepo = yield* ModelRepository.makeRepository( schema.organizationMembersTable, OrganizationMember.Model, diff --git a/packages/backend-core/src/repositories/rss-subscription-repo.ts b/packages/backend-core/src/repositories/rss-subscription-repo.ts index 8ca65699e..d6944eba9 100644 --- a/packages/backend-core/src/repositories/rss-subscription-repo.ts +++ b/packages/backend-core/src/repositories/rss-subscription-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import type { ChannelId, OrganizationId, RssSubscriptionId } from "@hazel/schema" import { RssSubscription } from "@hazel/domain/models" -import { ServiceMap, Effect, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" export class RssSubscriptionRepo extends ServiceMap.Service()("RssSubscriptionRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/typing-indicator-repo.ts b/packages/backend-core/src/repositories/typing-indicator-repo.ts index b4ba3ec50..d3813bfcd 100644 --- a/packages/backend-core/src/repositories/typing-indicator-repo.ts +++ b/packages/backend-core/src/repositories/typing-indicator-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, lt, ModelRepository, schema, type TxFn } from "@haze import { ChannelId, ChannelMemberId, TypingIndicatorId } from "@hazel/schema" import { TypingIndicator } from "@hazel/domain/models" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" export class TypingIndicatorRepo extends ServiceMap.Service()("TypingIndicatorRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/repositories/user-repo.ts b/packages/backend-core/src/repositories/user-repo.ts index 2b6fa1e65..3eccc1070 100644 --- a/packages/backend-core/src/repositories/user-repo.ts +++ b/packages/backend-core/src/repositories/user-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import type { UserId, WorkOSUserId } from "@hazel/schema" import { User } from "@hazel/domain/models" -import { ServiceMap, Effect, Option, type Schema } from "effect" +import { ServiceMap, Effect, Layer, Option, type Schema } from "effect" export class UserRepo extends ServiceMap.Service()("UserRepo", { make: Effect.gen(function* () { diff --git a/packages/backend-core/src/services/workos.ts b/packages/backend-core/src/services/workos.ts index e63cf3a6e..46835a036 100644 --- a/packages/backend-core/src/services/workos.ts +++ b/packages/backend-core/src/services/workos.ts @@ -1,5 +1,5 @@ import { WorkOS as WorkOSNodeAPI } from "@workos-inc/node" -import { ServiceMap, Config, Effect, Redacted, Schema } from "effect" +import { ServiceMap, Config, Effect, Layer, Redacted, Schema } from "effect" export class WorkOSApiError extends Schema.TaggedErrorClass()("WorkOSApiError", { cause: Schema.Unknown, diff --git a/packages/db/src/services/database.ts b/packages/db/src/services/database.ts index a6b63d080..89841508f 100644 --- a/packages/db/src/services/database.ts +++ b/packages/db/src/services/database.ts @@ -170,7 +170,7 @@ const makeService = (config: Config) => ), ) - const makeQueryWithSchema = ( + const makeQueryWithSchema = ( inputSchema: InputSchema, queryFn: ( execute: ( @@ -185,9 +185,9 @@ const makeService = (config: Config) => tx?: ( fn: (client: TransactionClient) => Promise, ) => Effect.Effect, - ): Effect.Effect => { + ): Effect.Effect => { return Effect.gen(function* () { - const validatedInput = yield* Schema.decodeEffect(inputSchema)(rawData) + const validatedInput = yield* Schema.decodeUnknownEffect(inputSchema)(rawData) if (tx) { return yield* queryFn(tx, validatedInput) @@ -252,7 +252,7 @@ const makeService = (config: Config) => } as const }) -type Shape = Effect.Effect.Success> +type Shape = Effect.Success> export class Database extends ServiceMap.Service()("Database") {} diff --git a/packages/db/src/services/drizzle-effect.ts b/packages/db/src/services/drizzle-effect.ts index 1d9060d33..2147f92d2 100644 --- a/packages/db/src/services/drizzle-effect.ts +++ b/packages/db/src/services/drizzle-effect.ts @@ -16,26 +16,26 @@ type ColumnSchema = TColumn["dataType"] extends : Schema.Schema : TColumn["dataType"] extends "bigint" ? TColumn extends { mode: "number" } - ? Schema.Schema - : Schema.Schema + ? Schema.Schema + : Schema.Schema : TColumn["dataType"] extends "number" ? TColumn["columnType"] extends `PgBigInt${number}` - ? Schema.Schema - : Schema.Schema + ? Schema.Schema + : Schema.Schema : TColumn["columnType"] extends "PgNumeric" - ? Schema.Schema + ? Schema.Schema : TColumn["columnType"] extends "PgUUID" ? Schema.Schema : TColumn["columnType"] extends "PgDate" ? TColumn extends { mode: "string" } - ? Schema.Schema - : Schema.Schema + ? Schema.Schema + : Schema.Schema : TColumn["columnType"] extends "PgTimestamp" ? TColumn extends { mode: "string" } - ? Schema.Schema - : Schema.Schema + ? Schema.Schema + : Schema.Schema : TColumn["dataType"] extends "string" - ? Schema.Schema + ? Schema.Schema : TColumn["dataType"] extends "boolean" ? Schema.Schema : TColumn["dataType"] extends "date" @@ -61,19 +61,19 @@ export const JsonValue = Schema.Union([ Schema.Number, Schema.Boolean, Schema.Null, - Schema.Record({ key: Schema.String, value: Schema.Unknown }), + Schema.Record(Schema.String, Schema.Unknown), Schema.Array(Schema.Unknown), ]) satisfies Schema.Schema // For cases where you need full JSON validation, use this explicit version -export const StrictJsonValue = Schema.suspend( - (): Schema.Schema => +export const StrictJsonValue: Schema.Schema = Schema.suspend( + () => Schema.Union([ Schema.String, Schema.Number, Schema.Boolean, Schema.Null, - Schema.Record({ key: Schema.String, value: StrictJsonValue }), + Schema.Record(Schema.String, StrictJsonValue), Schema.Array(StrictJsonValue), ]), ) @@ -102,29 +102,14 @@ type BuildRefine> = { } // Property signature builders - simplified +// In v4, optional properties are represented via Schema.optional/Schema.optionalKey wrappers type InsertProperty< TColumn extends Drizzle.Column, - TKey extends string, + _TKey extends string, > = TColumn["_"]["notNull"] extends false - ? Schema.PropertySignature< - "?:", - Schema.Schema.Type> | null | undefined, - TKey, - "?:", - Schema.Schema.Encoded> | null | undefined, - false, - never - > + ? Schema.optional>> : TColumn["_"]["hasDefault"] extends true - ? Schema.PropertySignature< - "?:", - Schema.Schema.Type> | undefined, - TKey, - "?:", - Schema.Schema.Encoded> | undefined, - true, - never - > + ? Schema.optional> : ColumnSchema type SelectProperty = TColumn["_"]["notNull"] extends false @@ -164,7 +149,7 @@ export function createInsertSchema = Object.fromEntries( + let schemaEntries: Record = Object.fromEntries( columnEntries.map(([name, column]) => [name, mapColumnToSchema(column)]), ) @@ -173,12 +158,11 @@ export function createInsertSchema [ name, typeof refineColumn === "function" && - !Schema.isSchema(refineColumn) && - !Schema.isPropertySignature(refineColumn) + !Schema.isSchema(refineColumn) ? ( refineColumn as ( - schema: Schema.Schema.All | Schema.PropertySignature.All, - ) => Schema.Schema.All | Schema.PropertySignature.All + schema: Schema.Top, + ) => Schema.Top )(schemaEntries[name]!) : refineColumn, ]) @@ -189,9 +173,9 @@ export function createInsertSchema = Object.fromEntries( + let schemaEntries: Record = Object.fromEntries( columnEntries.map(([name, column]) => [name, mapColumnToSchema(column)]), ) @@ -221,12 +205,11 @@ export function createSelectSchema [ name, typeof refineColumn === "function" && - !Schema.isSchema(refineColumn) && - !Schema.isPropertySignature(refineColumn) + !Schema.isSchema(refineColumn) ? ( refineColumn as ( - schema: Schema.Schema.All | Schema.PropertySignature.All, - ) => Schema.Schema.All | Schema.PropertySignature.All + schema: Schema.Top, + ) => Schema.Top )(schemaEntries[name]!) : refineColumn, ]) @@ -237,7 +220,7 @@ export function createSelectSchema { - let type: Schema.Schema | undefined +function mapColumnToSchema(column: Drizzle.Column): Schema.Schema { + let type: Schema.Schema | undefined if (isWithEnum(column)) { type = column.enumValues.length > 0 ? Schema.Literals(column.enumValues) : Schema.String @@ -293,7 +276,7 @@ function mapColumnToSchema(column: Drizzle.Column): Schema.Schema { Drizzle.is(column, DrizzleSqlite.SQLiteText)) && typeof column.length === "number" ) { - sType = sType.pipe(Schema.isMaxLength(column.length)) + sType = sType.check(Schema.isMaxLength(column.length)) } type = sType } diff --git a/packages/db/src/services/model-repository.ts b/packages/db/src/services/model-repository.ts index 67baafbd5..33cdaab92 100644 --- a/packages/db/src/services/model-repository.ts +++ b/packages/db/src/services/model-repository.ts @@ -1,6 +1,6 @@ import type { InferSelectModel, Table } from "drizzle-orm" import { eq } from "drizzle-orm" -import { pipe } from "effect" +import { pipe, Struct } from "effect" import * as Effect from "effect/Effect" import * as Option from "effect/Option" import * as Schema from "effect/Schema" @@ -28,16 +28,16 @@ export function makeRepository< db.makeQueryWithSchema(schema.insert as Schema.Schema, (execute, input) => execute((client) => client.insert(table).values([input]).returning()), )(data, tx), - ) as unknown as Effect.Effect + ) as unknown as Effect.Effect const insertVoid = (data: S["insert"]["Type"], tx?: TxFn) => db.makeQueryWithSchema(schema.insert as Schema.Schema, (execute, input) => execute((client) => client.insert(table).values(input)), - )(data, tx) as unknown as Effect.Effect + )(data, tx) as unknown as Effect.Effect const update = (data: S["update"]["Type"], tx?: TxFn) => db.makeQueryWithSchema( - Schema.partial(schema.update as Schema.Schema), + (schema.update as Schema.Struct).mapFields(Struct.map(Schema.optional)), (execute, input) => execute((client) => client @@ -53,11 +53,11 @@ export function makeRepository< : Effect.die(new EntityNotFound({ type: options.name, id: input[idColumn] })), ), ), - )(data, tx) as Effect.Effect + )(data, tx) as Effect.Effect const updateVoid = (data: S["update"]["Type"], tx?: TxFn) => db.makeQueryWithSchema( - Schema.partial(schema.update as Schema.Schema), + (schema.update as Schema.Struct).mapFields(Struct.map(Schema.optional)), (execute, input) => execute((client) => client @@ -66,7 +66,7 @@ export function makeRepository< // @ts-expect-error .where(eq(table[idColumn], input[idColumn])), ), - )(data, tx) as unknown as Effect.Effect + )(data, tx) as unknown as Effect.Effect const findById = (id: Id, tx?: TxFn) => db.makeQuery((execute, id: Id) => diff --git a/packages/db/src/services/model.ts b/packages/db/src/services/model.ts index d4961ddc3..a3e31047c 100644 --- a/packages/db/src/services/model.ts +++ b/packages/db/src/services/model.ts @@ -27,26 +27,26 @@ export interface Repository(fn: (client: TransactionClient) => Promise) => Effect.Effect, - ) => Effect.Effect + ) => Effect.Effect readonly insertVoid: ( insert: S["insert"]["Type"], tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, - ) => Effect.Effect + ) => Effect.Effect readonly update: ( update: PartialExcept, tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, - ) => Effect.Effect + ) => Effect.Effect readonly updateVoid: ( update: PartialExcept, tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, - ) => Effect.Effect + ) => Effect.Effect // readonly updateManyVoid: ( // update: PartialExcept[] - // ) => Effect.Effect + // ) => Effect.Effect readonly findById: ( id: Id, From 9f4c1d248591c4aa23a8ca6494aaa0eaa17fcc13 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 09:17:53 +0100 Subject: [PATCH 11/34] fix --- apps/backend/src/index.ts | 4 +- .../src/policies/channel-member-policy.ts | 257 +- .../policies/integration-connection-policy.ts | 4 +- .../src/policies/notification-policy.ts | 266 +- .../src/policies/organization-policy.ts | 99 +- .../src/policies/pinned-message-policy.ts | 179 +- apps/backend/src/routes/klipy.http.ts | 4 +- apps/backend/src/rpc/handlers/channels.ts | 4 +- apps/backend/src/rpc/middleware/auth-class.ts | 9 +- .../chat-sync/chat-sync-core-worker.ts | 13 +- .../chat-sync/discord-gateway-service.ts | 307 +- .../services/chat-sync/discord-sync-worker.ts | 4 +- .../src/services/integration-encryption.ts | 225 +- .../integrations/integration-bot-service.ts | 318 +- .../services/message-side-effect-service.ts | 4 +- .../src/services/mock-data-generator.ts | 4 +- .../src/services/oauth/oauth-http-client.ts | 16 +- .../services/oauth/oauth-provider-registry.ts | 185 +- apps/backend/src/services/rate-limiter.ts | 4 +- apps/backend/src/services/workos-webhook.ts | 289 +- apps/bot-gateway/src/index.ts | 21 +- apps/cluster/package.json | 2 +- apps/electric-proxy/src/tables/bot-tables.ts | 13 +- apps/link-preview-worker/src/cache.ts | 3 +- .../link-preview-worker/src/handlers/tweet.ts | 4 +- .../src/services/twitter.ts | 8 +- apps/web/src/atoms/desktop-auth.test.ts | 8 +- apps/web/src/lib/auth-token.ts | 4 +- apps/web/src/lib/error-messages.ts | 6 +- .../web/src/lib/services/common/api-client.ts | 8 +- .../src/lib/services/desktop/tauri-auth.ts | 2 +- .../lib/services/desktop/token-exchange.ts | 16 +- bots/hazel-bot/package.json | 2 +- bots/hazel-bot/src/agent-loop.ts | 8 +- bots/hazel-bot/src/errors.ts | 9 +- bots/hazel-bot/src/handler.ts | 8 +- bots/linear-bot/package.json | 2 +- bun.lock | 13 - libs/ai-openrouter/package.json | 21 - libs/ai-openrouter/src/Generated.ts | 6105 ----------------- libs/ai-openrouter/src/OpenRouterClient.ts | 373 - libs/ai-openrouter/src/OpenRouterConfig.ts | 78 - .../src/OpenRouterLanguageModel.ts | 1177 ---- libs/ai-openrouter/src/index.ts | 19 - libs/ai-openrouter/src/internal/utilities.ts | 26 - libs/ai-openrouter/tsconfig.json | 31 - libs/bot-sdk/src/errors.ts | 35 +- libs/bot-sdk/src/hazel-bot-sdk.ts | 3 +- libs/bot-sdk/src/rpc/client.ts | 9 +- libs/bot-sdk/src/services/health-server.ts | 3 +- libs/bot-sdk/src/streaming/errors.ts | 39 +- packages/actors/src/auth/config-service.ts | 17 +- packages/auth/src/cache/user-lookup-cache.ts | 24 +- .../auth/src/cache/user-lookup-request.ts | 32 +- packages/auth/src/config.ts | 3 +- packages/auth/src/consumers/backend-auth.ts | 14 +- packages/auth/src/consumers/proxy-auth.ts | 12 +- packages/auth/src/errors.ts | 11 +- packages/auth/src/metrics.ts | 16 +- packages/auth/src/session/jwt-decoder.ts | 2 +- packages/auth/src/session/workos-client.ts | 6 +- .../chat-sync-channel-link-repo.ts | 6 +- .../repositories/chat-sync-connection-repo.ts | 6 +- .../chat-sync-event-receipt-repo.ts | 6 +- .../chat-sync-message-link-repo.ts | 6 +- .../connect-conversation-channel-repo.ts | 6 +- .../repositories/connect-conversation-repo.ts | 6 +- .../repositories/connect-participant-repo.ts | 6 +- .../repositories/github-subscription-repo.ts | 6 +- .../integration-connection-repo.ts | 10 +- .../src/repositories/invitation-repo.ts | 4 +- .../repositories/organization-member-repo.ts | 8 +- .../src/repositories/typing-indicator-repo.ts | 2 +- .../repositories/user-presence-status-repo.ts | 12 +- .../src/repositories/user-repo.ts | 2 +- .../src/services/workos-sync.test.ts | 2 +- .../backend-core/src/services/workos-sync.ts | 120 +- packages/db/src/services/database.test.ts | 33 +- packages/db/src/services/database.ts | 10 +- packages/db/src/services/drizzle-effect.ts | 37 +- packages/db/src/services/model-repository.ts | 12 +- packages/db/src/services/model.ts | 8 +- .../cluster/activities/message-activities.ts | 5 +- .../activities/thread-naming-activities.ts | 11 +- packages/domain/src/current-user.ts | 9 +- packages/domain/src/desktop-auth-errors.ts | 9 +- packages/domain/src/errors.ts | 8 +- packages/domain/src/http/bot-commands.ts | 11 +- packages/domain/src/http/chat-sync.ts | 14 +- packages/domain/src/http/incoming-webhooks.ts | 21 +- .../domain/src/http/integration-resources.ts | 34 +- packages/domain/src/http/integrations.ts | 7 +- packages/domain/src/http/klipy.ts | 17 +- packages/domain/src/http/uploads.ts | 56 +- packages/domain/src/models/bot-model.ts | 4 +- .../models/integration-connection-model.ts | 9 +- .../domain/src/models/message-embed-schema.ts | 8 +- .../domain/src/models/organization-model.ts | 4 +- packages/domain/src/models/user-model.ts | 6 +- packages/domain/src/models/utils.ts | 68 +- packages/domain/src/rpc/bots.ts | 21 +- packages/domain/src/rpc/channel-members.ts | 5 +- packages/domain/src/rpc/channel-sections.ts | 5 +- packages/domain/src/rpc/channels.ts | 20 +- packages/domain/src/rpc/middleware.ts | 9 +- packages/domain/src/rpc/organizations.ts | 5 +- .../domain/src/scopes/current-bot-scopes.ts | 7 +- packages/domain/src/scopes/required-scopes.ts | 6 +- packages/domain/src/scopes/scope-map.ts | 4 +- packages/domain/src/scopes/validate-scopes.ts | 4 +- packages/domain/src/session-errors.ts | 4 +- packages/effect-bun/src/Redis.ts | 19 +- packages/effect-bun/src/S3.ts | 12 +- packages/effect-bun/src/Telemetry.ts | 5 +- .../src/persistence/redis-backing.ts | 86 +- packages/integrations/src/craft/api-client.ts | 53 +- .../integrations/src/discord/api-client.ts | 46 +- .../integrations/src/github/api-client.ts | 132 +- .../integrations/src/github/jwt-service.ts | 27 +- .../integrations/src/linear/api-client.ts | 66 +- packages/rivet-effect/src/actor.ts | 6 +- packages/schema/src/avatar-url.ts | 43 +- packages/schema/src/ids.ts | 378 +- packages/schema/src/workos.ts | 60 +- 124 files changed, 2153 insertions(+), 9867 deletions(-) delete mode 100644 libs/ai-openrouter/package.json delete mode 100644 libs/ai-openrouter/src/Generated.ts delete mode 100644 libs/ai-openrouter/src/OpenRouterClient.ts delete mode 100644 libs/ai-openrouter/src/OpenRouterConfig.ts delete mode 100644 libs/ai-openrouter/src/OpenRouterLanguageModel.ts delete mode 100644 libs/ai-openrouter/src/index.ts delete mode 100644 libs/ai-openrouter/src/internal/utilities.ts delete mode 100644 libs/ai-openrouter/tsconfig.json diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 74817e3ea..d74a2f5b9 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -91,9 +91,7 @@ export { HazelApi } // Export RPC groups for frontend consumption export { AuthMiddleware, InvitationRpcs, MessageRpcs, NotificationRpcs } from "@hazel/domain/rpc" -const HealthRouter = HttpRouter.use((router) => - router.add("GET", "/health", HttpServerResponse.text("OK")), -) +const HealthRouter = HttpRouter.use((router) => router.add("GET", "/health", HttpServerResponse.text("OK"))) const DocsRoute = HttpApiScalar.layerHttpRouter({ api: HazelApi, diff --git a/apps/backend/src/policies/channel-member-policy.ts b/apps/backend/src/policies/channel-member-policy.ts index 17ff435ed..8d010d02c 100644 --- a/apps/backend/src/policies/channel-member-policy.ts +++ b/apps/backend/src/policies/channel-member-policy.ts @@ -5,120 +5,54 @@ import { ServiceMap, Effect, Layer, Option } from "effect" import { isAdminOrOwner } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" -export class ChannelMemberPolicy extends ServiceMap.Service()("ChannelMemberPolicy/Policy", { - make: Effect.gen(function* () { - const policyEntity = "ChannelMember" as const - - const channelMemberRepo = yield* ChannelMemberRepo - const channelRepo = yield* ChannelRepo - const organizationMemberRepo = yield* OrganizationMemberRepo - const orgResolver = yield* OrgResolver - - const isOwner = (id: ChannelMemberId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "isOwner", - )( - channelMemberRepo.with(id, (member) => - policy( - policyEntity, - "isOwner", - Effect.fn(`${policyEntity}.isOwner`)(function* (actor) { - return yield* Effect.succeed(actor.id === member.userId) - }), - ), - ), - ) - - const canCreate = (channelId: ChannelId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "create", - )( - channelRepo.with(channelId, (channel) => - policy( - policyEntity, - "create", - Effect.fn(`${policyEntity}.create`)(function* (actor) { - const orgMember = yield* organizationMemberRepo.findByOrgAndUser( - channel.organizationId, - actor.id, - ) - - if (Option.isSome(orgMember) && isAdminOrOwner(orgMember.value.role)) { - return yield* Effect.succeed(true) - } - - // For public channels, any org member can join - if (channel.type === "public" && Option.isSome(orgMember)) { - return yield* Effect.succeed(true) - } - - return yield* Effect.succeed(false) - }), - ), - ), - ) - - const canRead = (channelId: ChannelId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "select", - )( - channelRepo.with(channelId, (channel) => - policy( - policyEntity, - "select", - Effect.fn(`${policyEntity}.select`)(function* (actor) { - // Check if user is a member of the channel - const membership = yield* channelMemberRepo.findByChannelAndUser( - channelId, - actor.id, - ) - - if (Option.isSome(membership)) { - return yield* Effect.succeed(true) - } - - // Organization admins can read all channel members - const orgMember = yield* organizationMemberRepo.findByOrgAndUser( - channel.organizationId, - actor.id, - ) - - if (Option.isSome(orgMember) && isAdminOrOwner(orgMember.value.role)) { - return yield* Effect.succeed(true) - } - - return yield* Effect.succeed(false) - }), +export class ChannelMemberPolicy extends ServiceMap.Service()( + "ChannelMemberPolicy/Policy", + { + make: Effect.gen(function* () { + const policyEntity = "ChannelMember" as const + + const channelMemberRepo = yield* ChannelMemberRepo + const channelRepo = yield* ChannelRepo + const organizationMemberRepo = yield* OrganizationMemberRepo + const orgResolver = yield* OrgResolver + + const isOwner = (id: ChannelMemberId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "isOwner", + )( + channelMemberRepo.with(id, (member) => + policy( + policyEntity, + "isOwner", + Effect.fn(`${policyEntity}.isOwner`)(function* (actor) { + return yield* Effect.succeed(actor.id === member.userId) + }), + ), ), - ), - ) - - const canUpdate = (id: ChannelMemberId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "update", - )( - channelMemberRepo.with(id, (member) => - channelRepo.with(member.channelId, (channel) => + ) + + const canCreate = (channelId: ChannelId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "create", + )( + channelRepo.with(channelId, (channel) => policy( policyEntity, - "update", - Effect.fn(`${policyEntity}.update`)(function* (actor) { - // Self-update always allowed - if (actor.id === member.userId) { - return yield* Effect.succeed(true) - } - - // Organization admins can update any membership + "create", + Effect.fn(`${policyEntity}.create`)(function* (actor) { const orgMember = yield* organizationMemberRepo.findByOrgAndUser( channel.organizationId, actor.id, ) - if (Option.isSome(orgMember) && orgMember.value.role === "admin") { + if (Option.isSome(orgMember) && isAdminOrOwner(orgMember.value.role)) { + return yield* Effect.succeed(true) + } + + // For public channels, any org member can join + if (channel.type === "public" && Option.isSome(orgMember)) { return yield* Effect.succeed(true) } @@ -126,32 +60,35 @@ export class ChannelMemberPolicy extends ServiceMap.Service }), ), ), - ), - ) - - const canDelete = (id: ChannelMemberId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "delete", - )( - channelMemberRepo.with(id, (member) => - channelRepo.with(member.channelId, (channel) => + ) + + const canRead = (channelId: ChannelId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "select", + )( + channelRepo.with(channelId, (channel) => policy( policyEntity, - "delete", - Effect.fn(`${policyEntity}.delete`)(function* (actor) { - // Self-removal always allowed - if (actor.id === member.userId) { + "select", + Effect.fn(`${policyEntity}.select`)(function* (actor) { + // Check if user is a member of the channel + const membership = yield* channelMemberRepo.findByChannelAndUser( + channelId, + actor.id, + ) + + if (Option.isSome(membership)) { return yield* Effect.succeed(true) } - // Organization admins can remove members + // Organization admins can read all channel members const orgMember = yield* organizationMemberRepo.findByOrgAndUser( channel.organizationId, actor.id, ) - if (Option.isSome(orgMember) && orgMember.value.role === "admin") { + if (Option.isSome(orgMember) && isAdminOrOwner(orgMember.value.role)) { return yield* Effect.succeed(true) } @@ -159,12 +96,78 @@ export class ChannelMemberPolicy extends ServiceMap.Service }), ), ), - ), - ) + ) + + const canUpdate = (id: ChannelMemberId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "update", + )( + channelMemberRepo.with(id, (member) => + channelRepo.with(member.channelId, (channel) => + policy( + policyEntity, + "update", + Effect.fn(`${policyEntity}.update`)(function* (actor) { + // Self-update always allowed + if (actor.id === member.userId) { + return yield* Effect.succeed(true) + } + + // Organization admins can update any membership + const orgMember = yield* organizationMemberRepo.findByOrgAndUser( + channel.organizationId, + actor.id, + ) + + if (Option.isSome(orgMember) && orgMember.value.role === "admin") { + return yield* Effect.succeed(true) + } + + return yield* Effect.succeed(false) + }), + ), + ), + ), + ) + + const canDelete = (id: ChannelMemberId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "delete", + )( + channelMemberRepo.with(id, (member) => + channelRepo.with(member.channelId, (channel) => + policy( + policyEntity, + "delete", + Effect.fn(`${policyEntity}.delete`)(function* (actor) { + // Self-removal always allowed + if (actor.id === member.userId) { + return yield* Effect.succeed(true) + } + + // Organization admins can remove members + const orgMember = yield* organizationMemberRepo.findByOrgAndUser( + channel.organizationId, + actor.id, + ) + + if (Option.isSome(orgMember) && orgMember.value.role === "admin") { + return yield* Effect.succeed(true) + } + + return yield* Effect.succeed(false) + }), + ), + ), + ), + ) - return { canCreate, canRead, canUpdate, canDelete, isOwner } as const - }), -}) { + return { canCreate, canRead, canUpdate, canDelete, isOwner } as const + }), + }, +) { static readonly layer = Layer.effect(this, this.make).pipe( Layer.provide(ChannelMemberRepo.layer), Layer.provide(ChannelRepo.layer), diff --git a/apps/backend/src/policies/integration-connection-policy.ts b/apps/backend/src/policies/integration-connection-policy.ts index 8f777bb18..1bf48e85a 100644 --- a/apps/backend/src/policies/integration-connection-policy.ts +++ b/apps/backend/src/policies/integration-connection-policy.ts @@ -56,7 +56,5 @@ export class IntegrationConnectionPolicy extends ServiceMap.Service()("NotificationPolicy/Policy", { - make: Effect.gen(function* () { - const policyEntity = "Notification" as const - - const notificationRepo = yield* NotificationRepo - const organizationMemberRepo = yield* OrganizationMemberRepo - - const canCreate = (_memberId: OrganizationMemberId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "create", - )( - policy( +export class NotificationPolicy extends ServiceMap.Service()( + "NotificationPolicy/Policy", + { + make: Effect.gen(function* () { + const policyEntity = "Notification" as const + + const notificationRepo = yield* NotificationRepo + const organizationMemberRepo = yield* OrganizationMemberRepo + + const canCreate = (_memberId: OrganizationMemberId) => + ErrorUtils.refailUnauthorized( policyEntity, "create", - Effect.fn(`${policyEntity}.create`)(function* (_actor) { - return yield* Effect.succeed(true) - }), - ), - ) - - const canView = (id: NotificationId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "view", - )( - notificationRepo.with(id, (notification) => + )( policy( policyEntity, - "view", - Effect.fn(`${policyEntity}.view`)(function* (actor) { - const member = yield* organizationMemberRepo.findById(notification.memberId) - - if (Option.isSome(member) && member.value.userId === actor.id) { - return yield* Effect.succeed(true) - } - - return yield* Effect.succeed(false) + "create", + Effect.fn(`${policyEntity}.create`)(function* (_actor) { + return yield* Effect.succeed(true) }), ), - ), - ) - - const canUpdate = (id: NotificationId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "update", - )( - notificationRepo.with(id, (notification) => - organizationMemberRepo.with(notification.memberId, (member) => + ) + + const canView = (id: NotificationId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "view", + )( + notificationRepo.with(id, (notification) => policy( policyEntity, - "update", - Effect.fn(`${policyEntity}.update`)(function* (actor) { - if (member.userId === actor.id) { - return yield* Effect.succeed(true) - } - - const actorMember = yield* organizationMemberRepo.findByOrgAndUser( - member.organizationId, - actor.id, - ) + "view", + Effect.fn(`${policyEntity}.view`)(function* (actor) { + const member = yield* organizationMemberRepo.findById(notification.memberId) - if (Option.isSome(actorMember)) { - return yield* Effect.succeed(isAdminOrOwner(actorMember.value.role)) + if (Option.isSome(member) && member.value.userId === actor.id) { + return yield* Effect.succeed(true) } return yield* Effect.succeed(false) }), ), ), - ), - ) - - const canDelete = (id: NotificationId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "delete", - )( - notificationRepo.with(id, (notification) => - organizationMemberRepo.with(notification.memberId, (member) => - policy( - policyEntity, - "delete", - Effect.fn(`${policyEntity}.delete`)(function* (actor) { - if (member.userId === actor.id) { - return yield* Effect.succeed(true) - } + ) - const actorMember = yield* organizationMemberRepo.findByOrgAndUser( - member.organizationId, - actor.id, - ) + const canUpdate = (id: NotificationId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "update", + )( + notificationRepo.with(id, (notification) => + organizationMemberRepo.with(notification.memberId, (member) => + policy( + policyEntity, + "update", + Effect.fn(`${policyEntity}.update`)(function* (actor) { + if (member.userId === actor.id) { + return yield* Effect.succeed(true) + } + + const actorMember = yield* organizationMemberRepo.findByOrgAndUser( + member.organizationId, + actor.id, + ) + + if (Option.isSome(actorMember)) { + return yield* Effect.succeed(isAdminOrOwner(actorMember.value.role)) + } + + return yield* Effect.succeed(false) + }), + ), + ), + ), + ) - if (Option.isSome(actorMember)) { - return yield* Effect.succeed(isAdminOrOwner(actorMember.value.role)) - } + const canDelete = (id: NotificationId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "delete", + )( + notificationRepo.with(id, (notification) => + organizationMemberRepo.with(notification.memberId, (member) => + policy( + policyEntity, + "delete", + Effect.fn(`${policyEntity}.delete`)(function* (actor) { + if (member.userId === actor.id) { + return yield* Effect.succeed(true) + } + + const actorMember = yield* organizationMemberRepo.findByOrgAndUser( + member.organizationId, + actor.id, + ) + + if (Option.isSome(actorMember)) { + return yield* Effect.succeed(isAdminOrOwner(actorMember.value.role)) + } + + return yield* Effect.succeed(false) + }), + ), + ), + ), + ) - return yield* Effect.succeed(false) - }), + const canMarkAsRead = (id: NotificationId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "markAsRead", + )( + notificationRepo.with(id, (notification) => + organizationMemberRepo.with(notification.memberId, (member) => + policy( + policyEntity, + "markAsRead", + Effect.fn(`${policyEntity}.markAsRead`)(function* (actor) { + if (member.userId === actor.id) { + return yield* Effect.succeed(true) + } + + const actorMember = yield* organizationMemberRepo.findByOrgAndUser( + member.organizationId, + actor.id, + ) + + if (Option.isSome(actorMember)) { + return yield* Effect.succeed(isAdminOrOwner(actorMember.value.role)) + } + + return yield* Effect.succeed(false) + }), + ), ), ), - ), - ) - - const canMarkAsRead = (id: NotificationId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "markAsRead", - )( - notificationRepo.with(id, (notification) => - organizationMemberRepo.with(notification.memberId, (member) => + ) + + const canMarkAllAsRead = (memberId: OrganizationMemberId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "markAllAsRead", + )( + organizationMemberRepo.with(memberId, (member) => policy( policyEntity, - "markAsRead", - Effect.fn(`${policyEntity}.markAsRead`)(function* (actor) { + "markAllAsRead", + Effect.fn(`${policyEntity}.markAllAsRead`)(function* (actor) { if (member.userId === actor.id) { return yield* Effect.succeed(true) } @@ -130,50 +162,22 @@ export class NotificationPolicy extends ServiceMap.Service() ) if (Option.isSome(actorMember)) { - return yield* Effect.succeed(isAdminOrOwner(actorMember.value.role)) + return yield* Effect.succeed( + actorMember.value.role === "admin" || + actorMember.value.role === "owner", + ) } return yield* Effect.succeed(false) }), ), ), - ), - ) - - const canMarkAllAsRead = (memberId: OrganizationMemberId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "markAllAsRead", - )( - organizationMemberRepo.with(memberId, (member) => - policy( - policyEntity, - "markAllAsRead", - Effect.fn(`${policyEntity}.markAllAsRead`)(function* (actor) { - if (member.userId === actor.id) { - return yield* Effect.succeed(true) - } - - const actorMember = yield* organizationMemberRepo.findByOrgAndUser( - member.organizationId, - actor.id, - ) - - if (Option.isSome(actorMember)) { - return yield* Effect.succeed( - actorMember.value.role === "admin" || actorMember.value.role === "owner", - ) - } - - return yield* Effect.succeed(false) - }), - ), - ), - ) + ) - return { canCreate, canView, canUpdate, canDelete, canMarkAsRead, canMarkAllAsRead } as const - }), -}) { + return { canCreate, canView, canUpdate, canDelete, canMarkAsRead, canMarkAllAsRead } as const + }), + }, +) { static readonly layer = Layer.effect(this, this.make).pipe( Layer.provide(NotificationRepo.layer), Layer.provide(OrganizationMemberRepo.layer), diff --git a/apps/backend/src/policies/organization-policy.ts b/apps/backend/src/policies/organization-policy.ts index 2a62490ff..522546187 100644 --- a/apps/backend/src/policies/organization-policy.ts +++ b/apps/backend/src/policies/organization-policy.ts @@ -4,51 +4,56 @@ import { ServiceMap, Effect, Layer } from "effect" import { makePolicy, withAnnotatedScope } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" -export class OrganizationPolicy extends ServiceMap.Service()("OrganizationPolicy/Policy", { - make: Effect.gen(function* () { - const policyEntity = "Organization" as const - const authorize = makePolicy(policyEntity) - - const orgResolver = yield* OrgResolver - - const canCreate = () => authorize("create", (_actor) => Effect.succeed(true)) - - const canUpdate = (id: OrganizationId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "update", - )( - withAnnotatedScope((scope) => - orgResolver.requireAdminOrOwner(id, scope, policyEntity, "update"), - ), - ) - - const isMember = (id: OrganizationId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "isMember", - )(withAnnotatedScope((scope) => orgResolver.requireScope(id, scope, policyEntity, "isMember"))) - - const canDelete = (id: OrganizationId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "delete", - )(withAnnotatedScope((scope) => orgResolver.requireOwner(id, scope, policyEntity, "delete"))) - - const canManagePublicInvite = (id: OrganizationId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "managePublicInvite", - )( - withAnnotatedScope((scope) => - orgResolver.requireAdminOrOwner(id, scope, policyEntity, "managePublicInvite"), - ), - ) - - return { canUpdate, canDelete, canCreate, isMember, canManagePublicInvite } as const - }), -}) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(OrgResolver.layer), - ) +export class OrganizationPolicy extends ServiceMap.Service()( + "OrganizationPolicy/Policy", + { + make: Effect.gen(function* () { + const policyEntity = "Organization" as const + const authorize = makePolicy(policyEntity) + + const orgResolver = yield* OrgResolver + + const canCreate = () => authorize("create", (_actor) => Effect.succeed(true)) + + const canUpdate = (id: OrganizationId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "update", + )( + withAnnotatedScope((scope) => + orgResolver.requireAdminOrOwner(id, scope, policyEntity, "update"), + ), + ) + + const isMember = (id: OrganizationId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "isMember", + )( + withAnnotatedScope((scope) => + orgResolver.requireScope(id, scope, policyEntity, "isMember"), + ), + ) + + const canDelete = (id: OrganizationId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "delete", + )(withAnnotatedScope((scope) => orgResolver.requireOwner(id, scope, policyEntity, "delete"))) + + const canManagePublicInvite = (id: OrganizationId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "managePublicInvite", + )( + withAnnotatedScope((scope) => + orgResolver.requireAdminOrOwner(id, scope, policyEntity, "managePublicInvite"), + ), + ) + + return { canUpdate, canDelete, canCreate, isMember, canManagePublicInvite } as const + }), + }, +) { + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(OrgResolver.layer)) } diff --git a/apps/backend/src/policies/pinned-message-policy.ts b/apps/backend/src/policies/pinned-message-policy.ts index 454d2f6f1..01e86431b 100644 --- a/apps/backend/src/policies/pinned-message-policy.ts +++ b/apps/backend/src/policies/pinned-message-policy.ts @@ -5,101 +5,73 @@ import { ServiceMap, Effect, Layer, Option } from "effect" import { isAdminOrOwner } from "../lib/policy-utils" import { OrgResolver } from "../services/org-resolver" -export class PinnedMessagePolicy extends ServiceMap.Service()("PinnedMessagePolicy/Policy", { - make: Effect.gen(function* () { - const policyEntity = "PinnedMessage" as const - - const pinnedMessageRepo = yield* PinnedMessageRepo - const channelRepo = yield* ChannelRepo - const organizationMemberRepo = yield* OrganizationMemberRepo - const orgResolver = yield* OrgResolver - - const canUpdate = (id: PinnedMessageId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "update", - )( - pinnedMessageRepo.with(id, (pinnedMessage) => - channelRepo.with(pinnedMessage.channelId, (channel) => +export class PinnedMessagePolicy extends ServiceMap.Service()( + "PinnedMessagePolicy/Policy", + { + make: Effect.gen(function* () { + const policyEntity = "PinnedMessage" as const + + const pinnedMessageRepo = yield* PinnedMessageRepo + const channelRepo = yield* ChannelRepo + const organizationMemberRepo = yield* OrganizationMemberRepo + const orgResolver = yield* OrgResolver + + const canUpdate = (id: PinnedMessageId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "update", + )( + pinnedMessageRepo.with(id, (pinnedMessage) => + channelRepo.with(pinnedMessage.channelId, (channel) => + policy( + policyEntity, + "update", + Effect.fn(`${policyEntity}.update`)(function* (actor) { + if (actor.id === pinnedMessage.pinnedBy) { + return yield* Effect.succeed(true) + } + + const orgMember = yield* organizationMemberRepo.findByOrgAndUser( + channel.organizationId, + actor.id, + ) + + if (Option.isSome(orgMember) && isAdminOrOwner(orgMember.value.role)) { + return yield* Effect.succeed(true) + } + + return yield* Effect.succeed(false) + }), + ), + ), + ), + ) + + const canCreate = (channelId: ChannelId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "create", + )( + channelRepo.with(channelId, (channel) => policy( policyEntity, - "update", - Effect.fn(`${policyEntity}.update`)(function* (actor) { - if (actor.id === pinnedMessage.pinnedBy) { - return yield* Effect.succeed(true) - } - + "create", + Effect.fn(`${policyEntity}.create`)(function* (actor) { const orgMember = yield* organizationMemberRepo.findByOrgAndUser( channel.organizationId, actor.id, ) - if (Option.isSome(orgMember) && isAdminOrOwner(orgMember.value.role)) { - return yield* Effect.succeed(true) + if (Option.isNone(orgMember)) { + return yield* Effect.succeed(false) } - return yield* Effect.succeed(false) - }), - ), - ), - ), - ) - - const canCreate = (channelId: ChannelId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "create", - )( - channelRepo.with(channelId, (channel) => - policy( - policyEntity, - "create", - Effect.fn(`${policyEntity}.create`)(function* (actor) { - const orgMember = yield* organizationMemberRepo.findByOrgAndUser( - channel.organizationId, - actor.id, - ) - - if (Option.isNone(orgMember)) { - return yield* Effect.succeed(false) - } - - if (isAdminOrOwner(orgMember.value.role)) { - return yield* Effect.succeed(true) - } - - // Regular members can pin in public channels - if (channel.type === "public") { - return yield* Effect.succeed(true) - } - - return yield* Effect.succeed(false) - }), - ), - ), - ) - - const canDelete = (id: PinnedMessageId) => - ErrorUtils.refailUnauthorized( - policyEntity, - "delete", - )( - pinnedMessageRepo.with(id, (pinnedMessage) => - channelRepo.with(pinnedMessage.channelId, (channel) => - policy( - policyEntity, - "delete", - Effect.fn(`${policyEntity}.delete`)(function* (actor) { - if (actor.id === pinnedMessage.pinnedBy) { + if (isAdminOrOwner(orgMember.value.role)) { return yield* Effect.succeed(true) } - const orgMember = yield* organizationMemberRepo.findByOrgAndUser( - channel.organizationId, - actor.id, - ) - - if (Option.isSome(orgMember) && isAdminOrOwner(orgMember.value.role)) { + // Regular members can pin in public channels + if (channel.type === "public") { return yield* Effect.succeed(true) } @@ -107,12 +79,43 @@ export class PinnedMessagePolicy extends ServiceMap.Service }), ), ), - ), - ) + ) + + const canDelete = (id: PinnedMessageId) => + ErrorUtils.refailUnauthorized( + policyEntity, + "delete", + )( + pinnedMessageRepo.with(id, (pinnedMessage) => + channelRepo.with(pinnedMessage.channelId, (channel) => + policy( + policyEntity, + "delete", + Effect.fn(`${policyEntity}.delete`)(function* (actor) { + if (actor.id === pinnedMessage.pinnedBy) { + return yield* Effect.succeed(true) + } + + const orgMember = yield* organizationMemberRepo.findByOrgAndUser( + channel.organizationId, + actor.id, + ) + + if (Option.isSome(orgMember) && isAdminOrOwner(orgMember.value.role)) { + return yield* Effect.succeed(true) + } + + return yield* Effect.succeed(false) + }), + ), + ), + ), + ) - return { canCreate, canDelete, canUpdate } as const - }), -}) { + return { canCreate, canDelete, canUpdate } as const + }), + }, +) { static readonly layer = Layer.effect(this, this.make).pipe( Layer.provide(PinnedMessageRepo.layer), Layer.provide(ChannelRepo.layer), diff --git a/apps/backend/src/routes/klipy.http.ts b/apps/backend/src/routes/klipy.http.ts index 14935967c..9a19244ea 100644 --- a/apps/backend/src/routes/klipy.http.ts +++ b/apps/backend/src/routes/klipy.http.ts @@ -86,10 +86,10 @@ const fetchKlipy = ( return response.json }), Effect.scoped, - Effect.catchTag("RequestError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail(new KlipyApiError({ message: `Klipy request failed: ${String(error)}` })), ), - Effect.catchTag("ResponseError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail(new KlipyApiError({ message: `Klipy response error: ${String(error)}` })), ), ) diff --git a/apps/backend/src/rpc/handlers/channels.ts b/apps/backend/src/rpc/handlers/channels.ts index 179da2f0a..c76da4dac 100644 --- a/apps/backend/src/rpc/handlers/channels.ts +++ b/apps/backend/src/rpc/handlers/channels.ts @@ -490,7 +490,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( ), // Workflow errors (ThreadChannelNotFoundError, AIProviderUnavailableError, etc.) // pass through directly since they're in the RPC union - only handle HTTP client errors - Effect.catchTag("RequestError", (err) => + Effect.catchTag("HttpClientError", (err) => Effect.fail( new WorkflowServiceUnavailableError({ message: "Cannot connect to workflow service", @@ -498,7 +498,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( }), ), ), - Effect.catchTag("ResponseError", (err) => + Effect.catchTag("HttpClientError", (err) => Effect.fail( new InternalServerError({ message: `Thread naming failed: ${err.reason}`, diff --git a/apps/backend/src/rpc/middleware/auth-class.ts b/apps/backend/src/rpc/middleware/auth-class.ts index 88a03cf6a..bc64ae184 100644 --- a/apps/backend/src/rpc/middleware/auth-class.ts +++ b/apps/backend/src/rpc/middleware/auth-class.ts @@ -57,9 +57,12 @@ const AuthFailure = S.Union([ WorkOSUserFetchError, ]) -export class AuthMiddleware extends RpcMiddleware.Service()("AuthMiddleware", { +export class AuthMiddleware extends RpcMiddleware.Service< + AuthMiddleware, + { + provides: CurrentUser.Context + } +>()("AuthMiddleware", { error: AuthFailure, requiredForClient: true, }) {} diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index 2da64ba32..769da1f98 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -72,11 +72,14 @@ export class DiscordSyncMessageNotFoundError extends Schema.TaggedErrorClass()("DiscordSyncApiError", { - message: Schema.String, - status: Schema.optional(Schema.Number), - detail: Schema.optional(Schema.String), -}) {} +export class DiscordSyncApiError extends Schema.TaggedErrorClass()( + "DiscordSyncApiError", + { + message: Schema.String, + status: Schema.optional(Schema.Number), + detail: Schema.optional(Schema.String), + }, +) {} type ChatSyncProvider = ChatSyncConnection.ChatSyncProvider diff --git a/apps/backend/src/services/chat-sync/discord-gateway-service.ts b/apps/backend/src/services/chat-sync/discord-gateway-service.ts index a00e79cdc..8797514fd 100644 --- a/apps/backend/src/services/chat-sync/discord-gateway-service.ts +++ b/apps/backend/src/services/chat-sync/discord-gateway-service.ts @@ -571,179 +571,184 @@ export const createDiscordGatewayDispatchHandlers = (deps: { } } -export class DiscordGatewayService extends ServiceMap.Service()("DiscordGatewayService", { - make: Effect.gen(function* () { - const discordSyncWorker = yield* DiscordSyncWorker - const channelLinkRepo = yield* ChatSyncChannelLinkRepo - - const gatewayEnabled = yield* Config.boolean("DISCORD_GATEWAY_ENABLED").pipe( - Config.withDefault(true), - Effect.orDie, - ) - const configuredIntents = yield* Config.number("DISCORD_GATEWAY_INTENTS").pipe( - // GUILDS + GUILD_MESSAGES + GUILD_MESSAGE_REACTIONS + MESSAGE_CONTENT - Config.withDefault(DISCORD_REQUIRED_GATEWAY_INTENTS), - Effect.orDie, - ) - const intents = configuredIntents | DISCORD_REQUIRED_GATEWAY_INTENTS - if (intents !== configuredIntents) { - yield* Effect.logWarning( - "DISCORD_GATEWAY_INTENTS missing required bits; forcing minimum intents", - { - configuredIntents, - effectiveIntents: intents, - }, +export class DiscordGatewayService extends ServiceMap.Service()( + "DiscordGatewayService", + { + make: Effect.gen(function* () { + const discordSyncWorker = yield* DiscordSyncWorker + const channelLinkRepo = yield* ChatSyncChannelLinkRepo + + const gatewayEnabled = yield* Config.boolean("DISCORD_GATEWAY_ENABLED").pipe( + Config.withDefault(true), + Effect.orDie, ) - } - const botTokenOption = yield* Config.redacted("DISCORD_BOT_TOKEN").pipe(Effect.option) + const configuredIntents = yield* Config.number("DISCORD_GATEWAY_INTENTS").pipe( + // GUILDS + GUILD_MESSAGES + GUILD_MESSAGE_REACTIONS + MESSAGE_CONTENT + Config.withDefault(DISCORD_REQUIRED_GATEWAY_INTENTS), + Effect.orDie, + ) + const intents = configuredIntents | DISCORD_REQUIRED_GATEWAY_INTENTS + if (intents !== configuredIntents) { + yield* Effect.logWarning( + "DISCORD_GATEWAY_INTENTS missing required bits; forcing minimum intents", + { + configuredIntents, + effectiveIntents: intents, + }, + ) + } + const botTokenOption = yield* Config.redacted("DISCORD_BOT_TOKEN").pipe(Effect.option) - if (!gatewayEnabled) { - yield* Effect.logInfo("Discord gateway disabled via DISCORD_GATEWAY_ENABLED=false") - return { - start: Effect.void, + if (!gatewayEnabled) { + yield* Effect.logInfo("Discord gateway disabled via DISCORD_GATEWAY_ENABLED=false") + return { + start: Effect.void, + } } - } - if (Option.isNone(botTokenOption)) { - yield* Effect.logWarning("Discord gateway disabled: DISCORD_BOT_TOKEN is not configured") - return { - start: Effect.void, + if (Option.isNone(botTokenOption)) { + yield* Effect.logWarning("Discord gateway disabled: DISCORD_BOT_TOKEN is not configured") + return { + start: Effect.void, + } } - } - const botToken = Redacted.value(botTokenOption.value) - const botUserIdRef = yield* Ref.make>(Option.none()) - - const DiscordLayer = DiscordLive.pipe( - Layer.provide( - DiscordConfig.layer({ - token: Redacted.make(botToken), - gateway: { intents }, - }), - ), - Layer.provide(BunSocket.layerWebSocketConstructor), - Layer.provide(FetchHttpClient.layer), - ) - - const isCurrentBotAuthor = (authorId?: string) => - Effect.gen(function* () { - if (!authorId) return false - const botUserId = yield* Ref.get(botUserIdRef) - return Option.isSome(botUserId) && botUserId.value === authorId - }) + const botToken = Redacted.value(botTokenOption.value) + const botUserIdRef = yield* Ref.make>(Option.none()) - const dispatchHandlers = createDiscordGatewayDispatchHandlers({ - discordSyncWorker, - findActiveLinksByExternalChannel: (externalChannelId) => - channelLinkRepo.findActiveByExternalChannel(externalChannelId), - isCurrentBotAuthor, - }) + const DiscordLayer = DiscordLive.pipe( + Layer.provide( + DiscordConfig.layer({ + token: Redacted.make(botToken), + gateway: { intents }, + }), + ), + Layer.provide(BunSocket.layerWebSocketConstructor), + Layer.provide(FetchHttpClient.layer), + ) - const onReady = Effect.fn("DiscordGatewayService.onReady")(function* (event: DiscordReadyEvent) { - if (!event.user?.id) { - yield* Effect.logWarning("Discord gateway READY payload missing bot user id") - return - } - const botUserId = decodeRequiredExternalId(event.user.id, decodeExternalUserId) - if (Option.isNone(botUserId)) { - yield* Effect.logWarning("Discord gateway READY payload has invalid bot user id", { - eventType: "READY", - field: "user.id", - valueType: getValueType(event.user.id), + const isCurrentBotAuthor = (authorId?: string) => + Effect.gen(function* () { + if (!authorId) return false + const botUserId = yield* Ref.get(botUserIdRef) + return Option.isSome(botUserId) && botUserId.value === authorId }) - return - } - yield* Ref.set(botUserIdRef, Option.some(botUserId.value)) - yield* Effect.logInfo("Discord gateway READY", { - botUserId: botUserId.value, + const dispatchHandlers = createDiscordGatewayDispatchHandlers({ + discordSyncWorker, + findActiveLinksByExternalChannel: (externalChannelId) => + channelLinkRepo.findActiveByExternalChannel(externalChannelId), + isCurrentBotAuthor, }) - }) - const onDispatchError = (eventType: string, error: unknown) => - Effect.logWarning("Discord gateway dispatch handler failed", { - eventType, - error: String(error), + const onReady = Effect.fn("DiscordGatewayService.onReady")(function* (event: DiscordReadyEvent) { + if (!event.user?.id) { + yield* Effect.logWarning("Discord gateway READY payload missing bot user id") + return + } + const botUserId = decodeRequiredExternalId(event.user.id, decodeExternalUserId) + if (Option.isNone(botUserId)) { + yield* Effect.logWarning("Discord gateway READY payload has invalid bot user id", { + eventType: "READY", + field: "user.id", + valueType: getValueType(event.user.id), + }) + return + } + + yield* Ref.set(botUserIdRef, Option.some(botUserId.value)) + yield* Effect.logInfo("Discord gateway READY", { + botUserId: botUserId.value, + }) }) - const start = Effect.gen(function* () { - yield* Effect.logInfo("Starting Discord gateway background worker with dfx", { - intents, - }) + const onDispatchError = (eventType: string, error: unknown) => + Effect.logWarning("Discord gateway dispatch handler failed", { + eventType, + error: String(error), + }) - yield* Effect.gen(function* () { - const gateway = yield* DiscordGateway + const start = Effect.gen(function* () { + yield* Effect.logInfo("Starting Discord gateway background worker with dfx", { + intents, + }) + + yield* Effect.gen(function* () { + const gateway = yield* DiscordGateway - yield* Effect.all( - [ - gateway.handleDispatch("READY", (event) => - onReady(event as DiscordReadyEvent).pipe( - Effect.catch((error) => onDispatchError("READY", error)), + yield* Effect.all( + [ + gateway.handleDispatch("READY", (event) => + onReady(event as DiscordReadyEvent).pipe( + Effect.catch((error) => onDispatchError("READY", error)), + ), + ), + gateway.handleDispatch("MESSAGE_CREATE", (event) => + dispatchHandlers + .ingestMessageCreateEvent(event as DiscordMessageCreateEvent) + .pipe(Effect.catch((error) => onDispatchError("MESSAGE_CREATE", error))), + ), + gateway.handleDispatch("MESSAGE_UPDATE", (event) => + dispatchHandlers + .ingestMessageUpdateEvent(event as DiscordMessageUpdateEvent) + .pipe(Effect.catch((error) => onDispatchError("MESSAGE_UPDATE", error))), + ), + gateway.handleDispatch("MESSAGE_DELETE", (event) => + dispatchHandlers + .ingestMessageDeleteEvent(event as DiscordMessageDeleteEvent) + .pipe(Effect.catch((error) => onDispatchError("MESSAGE_DELETE", error))), ), - ), - gateway.handleDispatch("MESSAGE_CREATE", (event) => - dispatchHandlers - .ingestMessageCreateEvent(event as DiscordMessageCreateEvent) - .pipe(Effect.catch((error) => onDispatchError("MESSAGE_CREATE", error))), - ), - gateway.handleDispatch("MESSAGE_UPDATE", (event) => - dispatchHandlers - .ingestMessageUpdateEvent(event as DiscordMessageUpdateEvent) - .pipe(Effect.catch((error) => onDispatchError("MESSAGE_UPDATE", error))), - ), - gateway.handleDispatch("MESSAGE_DELETE", (event) => - dispatchHandlers - .ingestMessageDeleteEvent(event as DiscordMessageDeleteEvent) - .pipe(Effect.catch((error) => onDispatchError("MESSAGE_DELETE", error))), - ), - gateway.handleDispatch("MESSAGE_REACTION_ADD", (event) => - dispatchHandlers - .ingestMessageReactionAddEvent(event as DiscordMessageReactionAddEvent) - .pipe( - Effect.catch((error) => - onDispatchError("MESSAGE_REACTION_ADD", error), + gateway.handleDispatch("MESSAGE_REACTION_ADD", (event) => + dispatchHandlers + .ingestMessageReactionAddEvent(event as DiscordMessageReactionAddEvent) + .pipe( + Effect.catch((error) => + onDispatchError("MESSAGE_REACTION_ADD", error), + ), ), - ), - ), - gateway.handleDispatch("MESSAGE_REACTION_REMOVE", (event) => - dispatchHandlers - .ingestMessageReactionRemoveEvent(event as DiscordMessageReactionRemoveEvent) - .pipe( - Effect.catch((error) => - onDispatchError("MESSAGE_REACTION_REMOVE", error), + ), + gateway.handleDispatch("MESSAGE_REACTION_REMOVE", (event) => + dispatchHandlers + .ingestMessageReactionRemoveEvent( + event as DiscordMessageReactionRemoveEvent, + ) + .pipe( + Effect.catch((error) => + onDispatchError("MESSAGE_REACTION_REMOVE", error), + ), ), - ), - ), - gateway.handleDispatch("THREAD_CREATE", (event) => - dispatchHandlers - .ingestThreadCreateEvent(event as DiscordThreadCreateEvent) - .pipe(Effect.catch((error) => onDispatchError("THREAD_CREATE", error))), - ), - ], - { - concurrency: "unbounded", - discard: true, - }, + ), + gateway.handleDispatch("THREAD_CREATE", (event) => + dispatchHandlers + .ingestThreadCreateEvent(event as DiscordThreadCreateEvent) + .pipe(Effect.catch((error) => onDispatchError("THREAD_CREATE", error))), + ), + ], + { + concurrency: "unbounded", + discard: true, + }, + ) + }).pipe( + Effect.provide(DiscordLayer), + Effect.catchCause((cause) => + Effect.logError("Discord gateway background worker stopped", { + cause: String(cause), + }), + ), + Effect.forkScoped, + Effect.asVoid, ) - }).pipe( - Effect.provide(DiscordLayer), - Effect.catchCause((cause) => - Effect.logError("Discord gateway background worker stopped", { - cause: String(cause), - }), - ), - Effect.forkScoped, - Effect.asVoid, - ) - }) + }) - yield* start + yield* start - return { - start: Effect.void, - } - }), -}) { + return { + start: Effect.void, + } + }), + }, +) { static readonly layer = Layer.effect(this, this.make).pipe( Layer.provide(DiscordSyncWorker.layer), Layer.provide(ChatSyncChannelLinkRepo.layer), diff --git a/apps/backend/src/services/chat-sync/discord-sync-worker.ts b/apps/backend/src/services/chat-sync/discord-sync-worker.ts index 8da21fa63..a7f1cefc7 100644 --- a/apps/backend/src/services/chat-sync/discord-sync-worker.ts +++ b/apps/backend/src/services/chat-sync/discord-sync-worker.ts @@ -233,7 +233,5 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(ChatSyncCoreWorker.layer), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(ChatSyncCoreWorker.layer)) } diff --git a/apps/backend/src/services/integration-encryption.ts b/apps/backend/src/services/integration-encryption.ts index 344c69c0a..d80cf63fc 100644 --- a/apps/backend/src/services/integration-encryption.ts +++ b/apps/backend/src/services/integration-encryption.ts @@ -23,128 +23,133 @@ export class KeyVersionNotFoundError extends Schema.TaggedErrorClass()("IntegrationEncryption", { - make: Effect.gen(function* () { - // Load encryption keys from config (support key rotation) - const currentKey = yield* Config.redacted("INTEGRATION_ENCRYPTION_KEY") - const currentKeyVersion = yield* Config.number("INTEGRATION_ENCRYPTION_KEY_VERSION").pipe( - Config.withDefault(1), - ) - - // Optional: Previous key for decryption during rotation - const previousKey = yield* Config.redacted("INTEGRATION_ENCRYPTION_KEY_PREV").pipe( - Config.option, - Effect.map(Option.getOrUndefined), - ) - const previousKeyVersion = yield* Config.number("INTEGRATION_ENCRYPTION_KEY_VERSION_PREV").pipe( - Config.withDefault(0), - ) - - // Cache for imported crypto keys - const keyCache = new Map() - - const importKey = (keyData: Redacted.Redacted, version: number) => - Effect.gen(function* () { - // Check cache first - const cachedKey = keyCache.get(version) - if (cachedKey) { - return cachedKey - } +export class IntegrationEncryption extends ServiceMap.Service()( + "IntegrationEncryption", + { + make: Effect.gen(function* () { + // Load encryption keys from config (support key rotation) + const currentKey = yield* Config.redacted("INTEGRATION_ENCRYPTION_KEY") + const currentKeyVersion = yield* Config.number("INTEGRATION_ENCRYPTION_KEY_VERSION").pipe( + Config.withDefault(1), + ) + + // Optional: Previous key for decryption during rotation + const previousKey = yield* Config.redacted("INTEGRATION_ENCRYPTION_KEY_PREV").pipe( + Config.option, + Effect.map(Option.getOrUndefined), + ) + const previousKeyVersion = yield* Config.number("INTEGRATION_ENCRYPTION_KEY_VERSION_PREV").pipe( + Config.withDefault(0), + ) + + // Cache for imported crypto keys + const keyCache = new Map() + + const importKey = (keyData: Redacted.Redacted, version: number) => + Effect.gen(function* () { + // Check cache first + const cachedKey = keyCache.get(version) + if (cachedKey) { + return cachedKey + } + + const rawKey = Buffer.from(Redacted.value(keyData), "base64") + + // Validate key length (256 bits = 32 bytes) + if (rawKey.length !== 32) { + return yield* Effect.fail( + new IntegrationEncryptionError({ + cause: `Invalid key length: expected 32 bytes, got ${rawKey.length}`, + operation: "importKey", + }), + ) + } + + const cryptoKey = yield* Effect.tryPromise({ + try: () => + crypto.subtle.importKey("raw", rawKey, { name: "AES-GCM", length: 256 }, false, [ + "encrypt", + "decrypt", + ]), + catch: (cause) => new IntegrationEncryptionError({ cause, operation: "importKey" }), + }) + + // Cache the imported key + keyCache.set(version, cryptoKey) + return cryptoKey + }) - const rawKey = Buffer.from(Redacted.value(keyData), "base64") + const encrypt = Effect.fn("IntegrationEncryption.encrypt")(function* (plaintext: string) { + const key = yield* importKey(currentKey, currentKeyVersion) - // Validate key length (256 bits = 32 bytes) - if (rawKey.length !== 32) { - return yield* Effect.fail( - new IntegrationEncryptionError({ - cause: `Invalid key length: expected 32 bytes, got ${rawKey.length}`, - operation: "importKey", - }), - ) - } + // Generate random 12-byte IV for AES-GCM + const iv = crypto.getRandomValues(new Uint8Array(12)) + const encoded = new TextEncoder().encode(plaintext) - const cryptoKey = yield* Effect.tryPromise({ + const ciphertext = yield* Effect.tryPromise({ try: () => - crypto.subtle.importKey("raw", rawKey, { name: "AES-GCM", length: 256 }, false, [ - "encrypt", - "decrypt", - ]), - catch: (cause) => new IntegrationEncryptionError({ cause, operation: "importKey" }), + crypto.subtle.encrypt( + { + name: "AES-GCM", + iv, + tagLength: 128, + }, + key, + encoded, + ), + catch: (cause) => new IntegrationEncryptionError({ cause, operation: "encrypt" }), }) - // Cache the imported key - keyCache.set(version, cryptoKey) - return cryptoKey + return { + ciphertext: Buffer.from(ciphertext).toString("base64"), + iv: Buffer.from(iv).toString("base64"), + keyVersion: currentKeyVersion, + } satisfies EncryptedToken }) - const encrypt = Effect.fn("IntegrationEncryption.encrypt")(function* (plaintext: string) { - const key = yield* importKey(currentKey, currentKeyVersion) - - // Generate random 12-byte IV for AES-GCM - const iv = crypto.getRandomValues(new Uint8Array(12)) - const encoded = new TextEncoder().encode(plaintext) - - const ciphertext = yield* Effect.tryPromise({ - try: () => - crypto.subtle.encrypt( - { - name: "AES-GCM", - iv, - tagLength: 128, - }, - key, - encoded, - ), - catch: (cause) => new IntegrationEncryptionError({ cause, operation: "encrypt" }), - }) + const decrypt = Effect.fn("IntegrationEncryption.decrypt")(function* (encrypted: EncryptedToken) { + // Select key based on version + let keyData: Redacted.Redacted | undefined + if (encrypted.keyVersion === currentKeyVersion) { + keyData = currentKey + } else if (previousKey && encrypted.keyVersion === previousKeyVersion) { + keyData = previousKey + } - return { - ciphertext: Buffer.from(ciphertext).toString("base64"), - iv: Buffer.from(iv).toString("base64"), - keyVersion: currentKeyVersion, - } satisfies EncryptedToken - }) - - const decrypt = Effect.fn("IntegrationEncryption.decrypt")(function* (encrypted: EncryptedToken) { - // Select key based on version - let keyData: Redacted.Redacted | undefined - if (encrypted.keyVersion === currentKeyVersion) { - keyData = currentKey - } else if (previousKey && encrypted.keyVersion === previousKeyVersion) { - keyData = previousKey - } + if (!keyData) { + return yield* Effect.fail( + new KeyVersionNotFoundError({ keyVersion: encrypted.keyVersion }), + ) + } - if (!keyData) { - return yield* Effect.fail(new KeyVersionNotFoundError({ keyVersion: encrypted.keyVersion })) - } + const key = yield* importKey(keyData, encrypted.keyVersion) + const iv = Buffer.from(encrypted.iv, "base64") + const ciphertext = Buffer.from(encrypted.ciphertext, "base64") - const key = yield* importKey(keyData, encrypted.keyVersion) - const iv = Buffer.from(encrypted.iv, "base64") - const ciphertext = Buffer.from(encrypted.ciphertext, "base64") - - const plaintext = yield* Effect.tryPromise({ - try: () => - crypto.subtle.decrypt( - { - name: "AES-GCM", - iv, - tagLength: 128, - }, - key, - ciphertext, - ), - catch: (cause) => new IntegrationEncryptionError({ cause, operation: "decrypt" }), - }) + const plaintext = yield* Effect.tryPromise({ + try: () => + crypto.subtle.decrypt( + { + name: "AES-GCM", + iv, + tagLength: 128, + }, + key, + ciphertext, + ), + catch: (cause) => new IntegrationEncryptionError({ cause, operation: "decrypt" }), + }) - return new TextDecoder().decode(plaintext) - }) + return new TextDecoder().decode(plaintext) + }) - return { - encrypt, - decrypt, - currentKeyVersion, - } - }), -}) { + return { + encrypt, + decrypt, + currentKeyVersion, + } + }), + }, +) { static readonly layer = Layer.effect(this, this.make) } diff --git a/apps/backend/src/services/integrations/integration-bot-service.ts b/apps/backend/src/services/integrations/integration-bot-service.ts index d00d2bde7..8509bacee 100644 --- a/apps/backend/src/services/integrations/integration-bot-service.ts +++ b/apps/backend/src/services/integrations/integration-bot-service.ts @@ -10,178 +10,184 @@ import { ServiceMap, Effect, Layer, Option } from "effect" * Manages global bot users for integration providers. * Each provider has a single shared bot user across all organizations. */ -export class IntegrationBotService extends ServiceMap.Service()("IntegrationBotService", { - make: Effect.gen(function* () { - const userRepo = yield* UserRepo - const orgMemberRepo = yield* OrganizationMemberRepo - const botRepo = yield* BotRepo - const botInstallationRepo = yield* BotInstallationRepo - - /** - * Get or create a global bot user for an OAuth integration provider. - * Bot users are machine users with predictable external IDs. - * Also ensures the bot is a member of the given organization so it appears in Electric sync. - */ - const getOrCreateBotUser = ( - provider: IntegrationConnection.IntegrationProvider, - organizationId: OrganizationId, - ) => - Effect.gen(function* () { - const externalId = `integration-bot-${provider}` - - // Try to find existing bot user - const existing = yield* userRepo.findByExternalId(externalId) - const botUser = Option.isSome(existing) - ? existing.value - : yield* Effect.gen(function* () { - // Create new machine user for this integration - const botConfig = Integrations.getBotConfig(provider) - const newUser = yield* userRepo.insert({ - externalId, - email: `${provider}-bot@integrations.internal`, - firstName: botConfig.name, - lastName: "", - avatarUrl: botConfig.avatarUrl, - userType: "machine", - settings: null, - isOnboarded: true, - timezone: null, - deletedAt: null, +export class IntegrationBotService extends ServiceMap.Service()( + "IntegrationBotService", + { + make: Effect.gen(function* () { + const userRepo = yield* UserRepo + const orgMemberRepo = yield* OrganizationMemberRepo + const botRepo = yield* BotRepo + const botInstallationRepo = yield* BotInstallationRepo + + /** + * Get or create a global bot user for an OAuth integration provider. + * Bot users are machine users with predictable external IDs. + * Also ensures the bot is a member of the given organization so it appears in Electric sync. + */ + const getOrCreateBotUser = ( + provider: IntegrationConnection.IntegrationProvider, + organizationId: OrganizationId, + ) => + Effect.gen(function* () { + const externalId = `integration-bot-${provider}` + + // Try to find existing bot user + const existing = yield* userRepo.findByExternalId(externalId) + const botUser = Option.isSome(existing) + ? existing.value + : yield* Effect.gen(function* () { + // Create new machine user for this integration + const botConfig = Integrations.getBotConfig(provider) + const newUser = yield* userRepo.insert({ + externalId, + email: `${provider}-bot@integrations.internal`, + firstName: botConfig.name, + lastName: "", + avatarUrl: botConfig.avatarUrl, + userType: "machine", + settings: null, + isOnboarded: true, + timezone: null, + deletedAt: null, + }) + + return newUser[0] }) - return newUser[0] - }) + // Ensure bot is a member of this organization (so it shows in Electric sync) + yield* orgMemberRepo.upsertByOrgAndUser({ + organizationId, + userId: botUser.id, + role: "member", + nickname: null, + joinedAt: new Date(), + invitedBy: null, + deletedAt: null, + }) - // Ensure bot is a member of this organization (so it shows in Electric sync) - yield* orgMemberRepo.upsertByOrgAndUser({ - organizationId, - userId: botUser.id, - role: "member", - nickname: null, - joinedAt: new Date(), - invitedBy: null, - deletedAt: null, + return botUser }) - return botUser - }) - - /** - * Get or create a global bot user for a webhook-based integration provider. - * Similar to OAuth providers but uses WEBHOOK_BOT_CONFIGS. - */ - const getOrCreateWebhookBotUser = ( - provider: Integrations.WebhookProvider, - organizationId: OrganizationId, - ) => - Effect.gen(function* () { - const externalId = `integration-bot-${provider}` - - // Try to find existing bot user - const existing = yield* userRepo.findByExternalId(externalId) - const botUser = Option.isSome(existing) - ? existing.value - : yield* Effect.gen(function* () { - // Create new machine user for this webhook integration - const botConfig = Integrations.getWebhookBotConfig(provider) - const newUser = yield* userRepo.insert({ - externalId, - email: `${provider}-bot@webhooks.internal`, - firstName: botConfig.name, - lastName: "", - avatarUrl: botConfig.avatarUrl, - userType: "machine", - settings: null, - isOnboarded: true, - timezone: null, - deletedAt: null, + /** + * Get or create a global bot user for a webhook-based integration provider. + * Similar to OAuth providers but uses WEBHOOK_BOT_CONFIGS. + */ + const getOrCreateWebhookBotUser = ( + provider: Integrations.WebhookProvider, + organizationId: OrganizationId, + ) => + Effect.gen(function* () { + const externalId = `integration-bot-${provider}` + + // Try to find existing bot user + const existing = yield* userRepo.findByExternalId(externalId) + const botUser = Option.isSome(existing) + ? existing.value + : yield* Effect.gen(function* () { + // Create new machine user for this webhook integration + const botConfig = Integrations.getWebhookBotConfig(provider) + const newUser = yield* userRepo.insert({ + externalId, + email: `${provider}-bot@webhooks.internal`, + firstName: botConfig.name, + lastName: "", + avatarUrl: botConfig.avatarUrl, + userType: "machine", + settings: null, + isOnboarded: true, + timezone: null, + deletedAt: null, + }) + + return newUser[0] }) - return newUser[0] - }) - - // Ensure bot is a member of this organization (so it shows in Electric sync) - yield* orgMemberRepo.upsertByOrgAndUser({ - organizationId, - userId: botUser.id, - role: "member", - nickname: null, - joinedAt: new Date(), - invitedBy: null, - deletedAt: null, - }) - - return botUser - }) - - /** - * Add an existing seeded bot to an organization. - * Unlike getOrCreateBotUser, this does NOT create the bot user - it must already exist from seeding. - * Creates org membership and bot installation. - * Returns Option.some(botUser) if found and added, Option.none() if bot not found. - */ - const addBotToOrg = ( - provider: IntegrationConnection.IntegrationProvider, - organizationId: OrganizationId, - ) => - Effect.gen(function* () { - const externalId = `internal-bot-${provider}` - - // Find existing bot user (must already exist from seed script) - const existingUser = yield* userRepo.findByExternalId(externalId) - if (Option.isNone(existingUser)) { - yield* Effect.logWarning("Bot user not found - has seed script been run?", { - provider, - externalId, + // Ensure bot is a member of this organization (so it shows in Electric sync) + yield* orgMemberRepo.upsertByOrgAndUser({ + organizationId, + userId: botUser.id, + role: "member", + nickname: null, + joinedAt: new Date(), + invitedBy: null, + deletedAt: null, }) - return Option.none() - } - const botUser = existingUser.value + return botUser + }) - // Find the bot record - const existingBot = yield* botRepo.findByUserId(botUser.id) - if (Option.isNone(existingBot)) { - yield* Effect.logWarning("Bot record not found for user - has seed script been run?", { - provider, + /** + * Add an existing seeded bot to an organization. + * Unlike getOrCreateBotUser, this does NOT create the bot user - it must already exist from seeding. + * Creates org membership and bot installation. + * Returns Option.some(botUser) if found and added, Option.none() if bot not found. + */ + const addBotToOrg = ( + provider: IntegrationConnection.IntegrationProvider, + organizationId: OrganizationId, + ) => + Effect.gen(function* () { + const externalId = `internal-bot-${provider}` + + // Find existing bot user (must already exist from seed script) + const existingUser = yield* userRepo.findByExternalId(externalId) + if (Option.isNone(existingUser)) { + yield* Effect.logWarning("Bot user not found - has seed script been run?", { + provider, + externalId, + }) + return Option.none() + } + + const botUser = existingUser.value + + // Find the bot record + const existingBot = yield* botRepo.findByUserId(botUser.id) + if (Option.isNone(existingBot)) { + yield* Effect.logWarning( + "Bot record not found for user - has seed script been run?", + { + provider, + userId: botUser.id, + }, + ) + return Option.none() + } + + const bot = existingBot.value + + // Add bot to org membership (so it shows in Electric sync) + yield* orgMemberRepo.upsertByOrgAndUser({ + organizationId, userId: botUser.id, + role: "member", + nickname: null, + joinedAt: new Date(), + invitedBy: null, + deletedAt: null, }) - return Option.none() - } - - const bot = existingBot.value - - // Add bot to org membership (so it shows in Electric sync) - yield* orgMemberRepo.upsertByOrgAndUser({ - organizationId, - userId: botUser.id, - role: "member", - nickname: null, - joinedAt: new Date(), - invitedBy: null, - deletedAt: null, - }) - - // Create bot installation (idempotent - check if exists first) - const existingInstallation = yield* botInstallationRepo.findByBotAndOrg( - bot.id, - organizationId, - ) - if (Option.isNone(existingInstallation)) { - yield* botInstallationRepo.insert({ - botId: bot.id, + // Create bot installation (idempotent - check if exists first) + const existingInstallation = yield* botInstallationRepo.findByBotAndOrg( + bot.id, organizationId, - installedBy: botUser.id, - }) - } + ) + + if (Option.isNone(existingInstallation)) { + yield* botInstallationRepo.insert({ + botId: bot.id, + organizationId, + installedBy: botUser.id, + }) + } - return Option.some(botUser) - }) + return Option.some(botUser) + }) - return { getOrCreateBotUser, getOrCreateWebhookBotUser, addBotToOrg } - }), -}) { + return { getOrCreateBotUser, getOrCreateWebhookBotUser, addBotToOrg } + }), + }, +) { static readonly layer = Layer.effect(this, this.make).pipe( Layer.provide(UserRepo.layer), Layer.provide(OrganizationMemberRepo.layer), diff --git a/apps/backend/src/services/message-side-effect-service.ts b/apps/backend/src/services/message-side-effect-service.ts index 5d541b1ba..9ce51dff8 100644 --- a/apps/backend/src/services/message-side-effect-service.ts +++ b/apps/backend/src/services/message-side-effect-service.ts @@ -286,7 +286,5 @@ export class MessageSideEffectService extends ServiceMap.Service()(" } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(DatabaseLive), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(DatabaseLive)) } diff --git a/apps/backend/src/services/oauth/oauth-http-client.ts b/apps/backend/src/services/oauth/oauth-http-client.ts index 05cc27f54..378013a87 100644 --- a/apps/backend/src/services/oauth/oauth-http-client.ts +++ b/apps/backend/src/services/oauth/oauth-http-client.ts @@ -220,10 +220,10 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut params: { code: string; redirectUri: string; clientId: string; clientSecret: string }, ) => exchangeCode(tokenUrl, params).pipe( - Effect.catchTag("TimeoutException", () => + Effect.catchTag("TimeoutError", () => Effect.fail(new OAuthHttpError({ message: "Request timed out" })), ), - Effect.catchTag("RequestError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new OAuthHttpError({ message: `Network error: ${String(error)}`, @@ -231,7 +231,7 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut }), ), ), - Effect.catchTag("ResponseError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new OAuthHttpError({ message: `Response error: ${String(error)}`, @@ -248,10 +248,10 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut params: { refreshToken: string; clientId: string; clientSecret: string }, ) => refreshToken(provider, params).pipe( - Effect.catchTag("TimeoutException", () => + Effect.catchTag("TimeoutError", () => Effect.fail(new OAuthHttpError({ message: "Request timed out" })), ), - Effect.catchTag("RequestError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new OAuthHttpError({ message: `Network error: ${String(error)}`, @@ -259,7 +259,7 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut }), ), ), - Effect.catchTag("ResponseError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new OAuthHttpError({ message: `Response error: ${String(error)}`, @@ -277,7 +277,5 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(FetchHttpClient.layer), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(FetchHttpClient.layer)) } diff --git a/apps/backend/src/services/oauth/oauth-provider-registry.ts b/apps/backend/src/services/oauth/oauth-provider-registry.ts index 902fe9b6e..a96405d3e 100644 --- a/apps/backend/src/services/oauth/oauth-provider-registry.ts +++ b/apps/backend/src/services/oauth/oauth-provider-registry.ts @@ -64,100 +64,103 @@ const SUPPORTED_PROVIDERS: readonly OAuthIntegrationProvider[] = ["linear", "git * 3. Add provider to SUPPORTED_PROVIDERS array * 4. Set environment variables: {PROVIDER}_CLIENT_ID, {PROVIDER}_CLIENT_SECRET, {PROVIDER}_REDIRECT_URI */ -export class OAuthProviderRegistry extends ServiceMap.Service()("OAuthProviderRegistry", { - make: Effect.gen(function* () { - // Cache for loaded providers - const providerCache = new Map() - - // Get the GitHub services for creating GitHub provider - const gitHubJwtService = yield* GitHub.GitHubAppJWTService - const gitHubApiClient = yield* GitHub.GitHubApiClient - - /** - * Get an OAuth provider instance. - * Loads configuration and creates provider on first access, then caches. - */ - const getProvider = ( - provider: IntegrationProvider, - ): Effect.Effect => - Effect.gen(function* () { - // Check cache first - const cached = providerCache.get(provider as OAuthIntegrationProvider) - if (cached) { - return cached - } - - // Check if provider is supported (cast needed since provider may include non-OAuth providers like "craft") - if (!(SUPPORTED_PROVIDERS as readonly string[]).includes(provider)) { - return yield* Effect.fail( - new UnsupportedProviderError({ - provider, - }), - ) - } - - // After the check above, we know the provider is a valid OAuth provider - const oauthProvider_ = provider as OAuthIntegrationProvider - - // Handle GitHub App separately (uses JWT service, not standard OAuth) - if (oauthProvider_ === "github") { - // Create provider with JWT service and API client - // Uses minimal AppProviderConfig since GitHub Apps manage their own auth via JWT - const oauthProvider = createGitHubAppProvider( - { provider: "github" }, - gitHubJwtService, - gitHubApiClient, +export class OAuthProviderRegistry extends ServiceMap.Service()( + "OAuthProviderRegistry", + { + make: Effect.gen(function* () { + // Cache for loaded providers + const providerCache = new Map() + + // Get the GitHub services for creating GitHub provider + const gitHubJwtService = yield* GitHub.GitHubAppJWTService + const gitHubApiClient = yield* GitHub.GitHubApiClient + + /** + * Get an OAuth provider instance. + * Loads configuration and creates provider on first access, then caches. + */ + const getProvider = ( + provider: IntegrationProvider, + ): Effect.Effect => + Effect.gen(function* () { + // Check cache first + const cached = providerCache.get(provider as OAuthIntegrationProvider) + if (cached) { + return cached + } + + // Check if provider is supported (cast needed since provider may include non-OAuth providers like "craft") + if (!(SUPPORTED_PROVIDERS as readonly string[]).includes(provider)) { + return yield* Effect.fail( + new UnsupportedProviderError({ + provider, + }), + ) + } + + // After the check above, we know the provider is a valid OAuth provider + const oauthProvider_ = provider as OAuthIntegrationProvider + + // Handle GitHub App separately (uses JWT service, not standard OAuth) + if (oauthProvider_ === "github") { + // Create provider with JWT service and API client + // Uses minimal AppProviderConfig since GitHub Apps manage their own auth via JWT + const oauthProvider = createGitHubAppProvider( + { provider: "github" }, + gitHubJwtService, + gitHubApiClient, + ) + providerCache.set(oauthProvider_, oauthProvider) + return oauthProvider + } + + // Get factory function for standard OAuth providers + const factory = PROVIDER_FACTORIES[oauthProvider_] + if (!factory) { + return yield* Effect.fail( + new UnsupportedProviderError({ + provider, + }), + ) + } + + // Load configuration from environment for standard OAuth providers + const config = yield* loadProviderConfig(oauthProvider_).pipe( + Effect.mapError( + (error) => + new ProviderNotConfiguredError({ + provider: oauthProvider_, + message: `Missing configuration for ${provider}: ${String(error)}`, + }), + ), ) + + // Create and cache provider + const oauthProvider = factory(config) providerCache.set(oauthProvider_, oauthProvider) + return oauthProvider - } - - // Get factory function for standard OAuth providers - const factory = PROVIDER_FACTORIES[oauthProvider_] - if (!factory) { - return yield* Effect.fail( - new UnsupportedProviderError({ - provider, - }), - ) - } - - // Load configuration from environment for standard OAuth providers - const config = yield* loadProviderConfig(oauthProvider_).pipe( - Effect.mapError( - (error) => - new ProviderNotConfiguredError({ - provider: oauthProvider_, - message: `Missing configuration for ${provider}: ${String(error)}`, - }), - ), - ) - - // Create and cache provider - const oauthProvider = factory(config) - providerCache.set(oauthProvider_, oauthProvider) - - return oauthProvider - }) - - /** - * List all supported/implemented providers. - */ - const listSupportedProviders = (): readonly OAuthIntegrationProvider[] => SUPPORTED_PROVIDERS - - /** - * Check if a provider is supported. - */ - const isProviderSupported = (provider: string): provider is OAuthIntegrationProvider => - (SUPPORTED_PROVIDERS as readonly string[]).includes(provider) - - return { - getProvider, - listSupportedProviders, - isProviderSupported, - } - }), -}) { + }) + + /** + * List all supported/implemented providers. + */ + const listSupportedProviders = (): readonly OAuthIntegrationProvider[] => SUPPORTED_PROVIDERS + + /** + * Check if a provider is supported. + */ + const isProviderSupported = (provider: string): provider is OAuthIntegrationProvider => + (SUPPORTED_PROVIDERS as readonly string[]).includes(provider) + + return { + getProvider, + listSupportedProviders, + isProviderSupported, + } + }), + }, +) { static readonly layer = Layer.effect(this, this.make).pipe( Layer.provide(GitHub.GitHubAppJWTService.layer), Layer.provide(GitHub.GitHubApiClient.layer), diff --git a/apps/backend/src/services/rate-limiter.ts b/apps/backend/src/services/rate-limiter.ts index 9c36259b1..ca54eeac3 100644 --- a/apps/backend/src/services/rate-limiter.ts +++ b/apps/backend/src/services/rate-limiter.ts @@ -98,9 +98,7 @@ export class RateLimiter extends ServiceMap.Service()("RateLimiter" } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(Redis.layer), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(Redis.layer)) } /** diff --git a/apps/backend/src/services/workos-webhook.ts b/apps/backend/src/services/workos-webhook.ts index 85fcdf7e0..c9ddfd5e5 100644 --- a/apps/backend/src/services/workos-webhook.ts +++ b/apps/backend/src/services/workos-webhook.ts @@ -8,14 +8,13 @@ export class WebhookVerificationError extends Schema.TaggedErrorClass("WebhookTimestampError")( +export class WebhookTimestampError extends Schema.TaggedErrorClass( "WebhookTimestampError", - { - message: Schema.String, - timestamp: Schema.Number, - currentTime: Schema.Number, - }, -) {} +)("WebhookTimestampError", { + message: Schema.String, + timestamp: Schema.Number, + currentTime: Schema.Number, +}) {} export interface WorkOSWebhookSignature { timestamp: number @@ -24,140 +23,146 @@ export interface WorkOSWebhookSignature { const DEFAULT_TIMESTAMP_TOLERANCE = Duration.minutes(5) -export class WorkOSWebhookVerifier extends ServiceMap.Service()("WorkOSWebhookVerifier", { - make: Effect.gen(function* () { - // Get webhook secret from config - const webhookSecret = yield* Config.string("WORKOS_WEBHOOK_SECRET") - - /** - * Parse the WorkOS-Signature header - * Format: "t=,v1=" - */ - const parseSignatureHeader = ( - header: string, - ): Effect.Effect => - Effect.gen(function* () { - const parts = header.split(",").map((p) => p.trim()) - if (parts.length !== 2) { - return yield* Effect.fail( - new WebhookVerificationError({ - message: `Invalid signature header format: expected 2 parts (t=...,v1=...), got ${parts.length}. Header: '${header.slice(0, 50)}${header.length > 50 ? "..." : ""}'`, - }), - ) - } - - const timestampPart = parts[0] - const signaturePart = parts[1] - - if (!timestampPart.startsWith("t=") || !signaturePart.startsWith("v1=")) { - return yield* Effect.fail( - new WebhookVerificationError({ - message: `Invalid signature header format: expected 't=,v1=', got '${timestampPart.slice(0, 15)}...,${signaturePart.slice(0, 15)}...'`, - }), - ) - } - - const timestamp = parseInt(timestampPart.slice(2), 10) - const signature = signaturePart.slice(3) - - if (Number.isNaN(timestamp)) { - return yield* Effect.fail( - new WebhookVerificationError({ - message: `Invalid timestamp in signature header: '${timestampPart}' is not a valid number`, - }), - ) - } - - return { timestamp, signature } - }) - - /** - * Validate timestamp to prevent replay attacks - * Default tolerance is 5 minutes - */ - const validateTimestamp = ( - timestamp: number, - tolerance: Duration.Duration = DEFAULT_TIMESTAMP_TOLERANCE, - ): Effect.Effect => - Effect.gen(function* () { - const webhookTime = DateTime.unsafeMake(timestamp) - const now = DateTime.unsafeNow() - const difference = DateTime.distanceDuration(webhookTime, now) - - if (Duration.greaterThan(difference, tolerance)) { - return yield* Effect.fail( - new WebhookTimestampError({ - message: `Webhook timestamp is too old. Difference: ${Duration.format(difference)}, Tolerance: ${Duration.format(tolerance)}`, - timestamp, - currentTime: Date.now(), - }), - ) - } - }) - - /** - * Compute the expected signature using HMAC SHA256 - */ - const computeSignature = (timestamp: number, payload: string): string => { - const signedPayload = `${timestamp}.${payload}` - const hmac = crypto.createHmac("sha256", webhookSecret) - hmac.update(signedPayload) - return hmac.digest("hex") - } - - /** - * Verify the webhook signature - */ - const verifyWebhook = ( - signatureHeader: string, - payload: string, - options?: { - timestampTolerance?: Duration.Duration - }, - ): Effect.Effect => - Effect.gen(function* () { - // Parse the signature header - const { timestamp, signature } = yield* parseSignatureHeader(signatureHeader) - - // Validate timestamp - yield* validateTimestamp(timestamp, options?.timestampTolerance) - - // Compute expected signature - const expectedSignature = computeSignature(timestamp, payload) - - // Compare signatures using timing-safe comparison - const signatureBuffer = Buffer.from(signature, "hex") - const expectedBuffer = Buffer.from(expectedSignature, "hex") - - if (signatureBuffer.length !== expectedBuffer.length) { - return yield* Effect.fail( - new WebhookVerificationError({ - message: `Signature length mismatch: received ${signatureBuffer.length} bytes, expected ${expectedBuffer.length} bytes`, - }), - ) - } - - if ( - !crypto.timingSafeEqual(new Uint8Array(signatureBuffer), new Uint8Array(expectedBuffer)) - ) { - return yield* Effect.fail( - new WebhookVerificationError({ - message: - "Webhook signature does not match. This could indicate the webhook secret is incorrect or the payload was modified.", - }), - ) - } - - yield* Effect.logInfo("WorkOS webhook signature verified successfully") - }) - - return { - verifyWebhook, - parseSignatureHeader, - validateTimestamp, - computeSignature, - } - }), -}) { +export class WorkOSWebhookVerifier extends ServiceMap.Service()( + "WorkOSWebhookVerifier", + { + make: Effect.gen(function* () { + // Get webhook secret from config + const webhookSecret = yield* Config.string("WORKOS_WEBHOOK_SECRET") + + /** + * Parse the WorkOS-Signature header + * Format: "t=,v1=" + */ + const parseSignatureHeader = ( + header: string, + ): Effect.Effect => + Effect.gen(function* () { + const parts = header.split(",").map((p) => p.trim()) + if (parts.length !== 2) { + return yield* Effect.fail( + new WebhookVerificationError({ + message: `Invalid signature header format: expected 2 parts (t=...,v1=...), got ${parts.length}. Header: '${header.slice(0, 50)}${header.length > 50 ? "..." : ""}'`, + }), + ) + } + + const timestampPart = parts[0] + const signaturePart = parts[1] + + if (!timestampPart.startsWith("t=") || !signaturePart.startsWith("v1=")) { + return yield* Effect.fail( + new WebhookVerificationError({ + message: `Invalid signature header format: expected 't=,v1=', got '${timestampPart.slice(0, 15)}...,${signaturePart.slice(0, 15)}...'`, + }), + ) + } + + const timestamp = parseInt(timestampPart.slice(2), 10) + const signature = signaturePart.slice(3) + + if (Number.isNaN(timestamp)) { + return yield* Effect.fail( + new WebhookVerificationError({ + message: `Invalid timestamp in signature header: '${timestampPart}' is not a valid number`, + }), + ) + } + + return { timestamp, signature } + }) + + /** + * Validate timestamp to prevent replay attacks + * Default tolerance is 5 minutes + */ + const validateTimestamp = ( + timestamp: number, + tolerance: Duration.Duration = DEFAULT_TIMESTAMP_TOLERANCE, + ): Effect.Effect => + Effect.gen(function* () { + const webhookTime = DateTime.unsafeMake(timestamp) + const now = DateTime.unsafeNow() + const difference = DateTime.distanceDuration(webhookTime, now) + + if (Duration.greaterThan(difference, tolerance)) { + return yield* Effect.fail( + new WebhookTimestampError({ + message: `Webhook timestamp is too old. Difference: ${Duration.format(difference)}, Tolerance: ${Duration.format(tolerance)}`, + timestamp, + currentTime: Date.now(), + }), + ) + } + }) + + /** + * Compute the expected signature using HMAC SHA256 + */ + const computeSignature = (timestamp: number, payload: string): string => { + const signedPayload = `${timestamp}.${payload}` + const hmac = crypto.createHmac("sha256", webhookSecret) + hmac.update(signedPayload) + return hmac.digest("hex") + } + + /** + * Verify the webhook signature + */ + const verifyWebhook = ( + signatureHeader: string, + payload: string, + options?: { + timestampTolerance?: Duration.Duration + }, + ): Effect.Effect => + Effect.gen(function* () { + // Parse the signature header + const { timestamp, signature } = yield* parseSignatureHeader(signatureHeader) + + // Validate timestamp + yield* validateTimestamp(timestamp, options?.timestampTolerance) + + // Compute expected signature + const expectedSignature = computeSignature(timestamp, payload) + + // Compare signatures using timing-safe comparison + const signatureBuffer = Buffer.from(signature, "hex") + const expectedBuffer = Buffer.from(expectedSignature, "hex") + + if (signatureBuffer.length !== expectedBuffer.length) { + return yield* Effect.fail( + new WebhookVerificationError({ + message: `Signature length mismatch: received ${signatureBuffer.length} bytes, expected ${expectedBuffer.length} bytes`, + }), + ) + } + + if ( + !crypto.timingSafeEqual( + new Uint8Array(signatureBuffer), + new Uint8Array(expectedBuffer), + ) + ) { + return yield* Effect.fail( + new WebhookVerificationError({ + message: + "Webhook signature does not match. This could indicate the webhook secret is incorrect or the payload was modified.", + }), + ) + } + + yield* Effect.logInfo("WorkOS webhook signature verified successfully") + }) + + return { + verifyWebhook, + parseSignatureHeader, + validateTimestamp, + computeSignature, + } + }), + }, +) { static readonly layer = Layer.effect(this, this.make) } diff --git a/apps/bot-gateway/src/index.ts b/apps/bot-gateway/src/index.ts index 92629f392..435494dc4 100644 --- a/apps/bot-gateway/src/index.ts +++ b/apps/bot-gateway/src/index.ts @@ -159,11 +159,14 @@ class GatewayProtocolError extends Schema.TaggedErrorClass message: Schema.String, }) {} -export class GatewayStartupError extends Schema.TaggedErrorClass()("GatewayStartupError", { - dependency: Schema.Literals(["config", "database", "redis", "tracer", "server"]), - message: Schema.String, - cause: Schema.optional(Schema.Unknown), -}) {} +export class GatewayStartupError extends Schema.TaggedErrorClass()( + "GatewayStartupError", + { + dependency: Schema.Literals(["config", "database", "redis", "tracer", "server"]), + message: Schema.String, + cause: Schema.optional(Schema.Unknown), + }, +) {} class DurableStreamGatewayError extends Schema.TaggedErrorClass()( "DurableStreamGatewayError", @@ -477,9 +480,11 @@ class BotGatewayHub extends ServiceMap.Service()("BotGatewayHub", const ackResult = yield* Deferred.await(ackDeferred).pipe( Effect.timeoutOrElse({ onTimeout: () => - Effect.fail(new GatewayProtocolError({ - message: `Timed out waiting for ACK from session ${id}`, - })), + Effect.fail( + new GatewayProtocolError({ + message: `Timed out waiting for ACK from session ${id}`, + }), + ), duration: config.batchAckTimeoutMs, }), ) diff --git a/apps/cluster/package.json b/apps/cluster/package.json index 21425fdbc..51bbb568c 100644 --- a/apps/cluster/package.json +++ b/apps/cluster/package.json @@ -9,9 +9,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@effect/ai-openrouter": "catalog:effect", "@effect/platform-bun": "catalog:effect", "@effect/sql-pg": "catalog:effect", - "@effect/ai-openrouter": "catalog:effect", "@hazel/backend-core": "workspace:*", "@hazel/db": "workspace:*", "@hazel/domain": "workspace:*", diff --git a/apps/electric-proxy/src/tables/bot-tables.ts b/apps/electric-proxy/src/tables/bot-tables.ts index 99ad37377..53b8b3ba9 100644 --- a/apps/electric-proxy/src/tables/bot-tables.ts +++ b/apps/electric-proxy/src/tables/bot-tables.ts @@ -6,11 +6,14 @@ import type { WhereClauseResult } from "./where-clause-builder" /** * Error thrown when bot table access is denied or where clause cannot be generated */ -export class BotTableAccessError extends Schema.TaggedErrorClass()("BotTableAccessError", { - message: Schema.String, - detail: Schema.optional(Schema.String), - table: Schema.String, -}) {} +export class BotTableAccessError extends Schema.TaggedErrorClass()( + "BotTableAccessError", + { + message: Schema.String, + detail: Schema.optional(Schema.String), + table: Schema.String, + }, +) {} /** * Tables that bots can access through the Electric proxy. diff --git a/apps/link-preview-worker/src/cache.ts b/apps/link-preview-worker/src/cache.ts index f9eac6131..6c9302cf9 100644 --- a/apps/link-preview-worker/src/cache.ts +++ b/apps/link-preview-worker/src/cache.ts @@ -9,7 +9,8 @@ const CACHE_TTL = 3600 * KV Cache Service * Provides caching functionality using Cloudflare KV */ -export class KVCache extends ServiceMap.Service(key: string) => Effect.Effect set: (key: string, value: T) => Effect.Effect diff --git a/apps/link-preview-worker/src/handlers/tweet.ts b/apps/link-preview-worker/src/handlers/tweet.ts index 4961e1ec1..82067b164 100644 --- a/apps/link-preview-worker/src/handlers/tweet.ts +++ b/apps/link-preview-worker/src/handlers/tweet.ts @@ -15,9 +15,7 @@ export const HttpTweetLive = HttpApiBuilder.group(LinkPreviewApi, "tweet", (hand const twitterApi = yield* TwitterApi // Check cache first - const cachedData = yield* cache - .get(cacheKey) - .pipe(Effect.catch(() => Effect.succeed(null))) + const cachedData = yield* cache.get(cacheKey).pipe(Effect.catch(() => Effect.succeed(null))) if (cachedData) { yield* Effect.logDebug(`Cache hit for tweet: ${tweetId}`) diff --git a/apps/link-preview-worker/src/services/twitter.ts b/apps/link-preview-worker/src/services/twitter.ts index 6addeb4a3..f2dda2869 100644 --- a/apps/link-preview-worker/src/services/twitter.ts +++ b/apps/link-preview-worker/src/services/twitter.ts @@ -108,9 +108,7 @@ export class TwitterApi extends ServiceMap.Service()("TwitterApi", { ) // Parse JSON response - const data: any = yield* response.json.pipe( - Effect.catch(() => Effect.succeed(undefined)), - ) + const data: any = yield* response.json.pipe(Effect.catch(() => Effect.succeed(undefined))) // Handle successful response if (response.status >= 200 && response.status < 300) { @@ -168,7 +166,5 @@ export class TwitterApi extends ServiceMap.Service()("TwitterApi", { } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(FetchHttpClient.layer), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(FetchHttpClient.layer)) } diff --git a/apps/web/src/atoms/desktop-auth.test.ts b/apps/web/src/atoms/desktop-auth.test.ts index 17319f692..11a224bc0 100644 --- a/apps/web/src/atoms/desktop-auth.test.ts +++ b/apps/web/src/atoms/desktop-auth.test.ts @@ -35,7 +35,7 @@ describe("isFatalRefreshError", () => { it("returns false for timeout errors", () => { const error = { - _tag: "TimeoutException", + _tag: "TimeoutError", message: "Request timed out", } expect(isFatalRefreshError(error)).toBe(false) @@ -43,7 +43,7 @@ describe("isFatalRefreshError", () => { it("returns false for network errors", () => { const error = { - _tag: "RequestError", + _tag: "HttpClientError", message: "Network error during token refresh", } expect(isFatalRefreshError(error)).toBe(false) @@ -65,7 +65,7 @@ describe("isFatalRefreshError", () => { describe("isTransientError", () => { it("returns true for TimeoutException tag", () => { const error = { - _tag: "TimeoutException", + _tag: "TimeoutError", message: "Operation timed out", } expect(isTransientError(error)).toBe(true) @@ -73,7 +73,7 @@ describe("isTransientError", () => { it("returns true for RequestError tag", () => { const error = { - _tag: "RequestError", + _tag: "HttpClientError", message: "Request failed", } expect(isTransientError(error)).toBe(true) diff --git a/apps/web/src/lib/auth-token.ts b/apps/web/src/lib/auth-token.ts index 25e2b757b..f0e3bd3be 100644 --- a/apps/web/src/lib/auth-token.ts +++ b/apps/web/src/lib/auth-token.ts @@ -60,8 +60,8 @@ export const isTransientError = (error: { _tag?: string; message?: string }): bo message.includes("timed out") || message.includes("timeout") || message.includes("network error") || - error._tag === "TimeoutException" || - error._tag === "RequestError" + error._tag === "TimeoutError" || + error._tag === "HttpClientError" ) } diff --git a/apps/web/src/lib/error-messages.ts b/apps/web/src/lib/error-messages.ts index e242f5f1b..35d9e822b 100644 --- a/apps/web/src/lib/error-messages.ts +++ b/apps/web/src/lib/error-messages.ts @@ -449,7 +449,7 @@ const isSchemaCommonError = Schema.is(CommonAppErrorSchema) /** * Tags for non-Schema errors that are still common */ -const NON_SCHEMA_COMMON_TAGS = new Set(["ParseError", "RequestError", "ResponseError"]) +const NON_SCHEMA_COMMON_TAGS = new Set(["ParseError", "HttpClientError", "HttpClientError"]) /** * Type guard for CommonAppError. @@ -474,7 +474,7 @@ function isNetworkError(error: unknown): boolean { if (typeof error !== "object" || error === null) return false // Effect HttpClientError.RequestError with Transport reason - if ("_tag" in error && error._tag === "RequestError" && "reason" in error) { + if ("_tag" in error && error._tag === "HttpClientError" && "reason" in error) { return (error as { reason: string }).reason === "Transport" } @@ -492,7 +492,7 @@ function isNetworkError(error: unknown): boolean { function isTimeoutError(error: unknown): boolean { if (typeof error !== "object" || error === null) return false if ("_tag" in error) { - return (error as { _tag: string })._tag === "TimeoutException" + return (error as { _tag: string })._tag === "TimeoutError" } return false } diff --git a/apps/web/src/lib/services/common/api-client.ts b/apps/web/src/lib/services/common/api-client.ts index 65656f464..e789d4fe1 100644 --- a/apps/web/src/lib/services/common/api-client.ts +++ b/apps/web/src/lib/services/common/api-client.ts @@ -26,21 +26,19 @@ export class ApiClient extends ServiceMap.Service()("ApiClient", { times: 3, // Only retry server errors (5xx), not client errors (4xx) like 401/403 while: (error) => { - if (error._tag === "ResponseError") { + if (error._tag === "HttpClientError") { const status = error.response.status // Only retry server errors (500-599) and network errors // Don't retry client errors (400-499) including auth errors return status >= 500 && status < 600 } // Retry other transient errors (network issues, etc.) - return error._tag === "RequestError" + return error._tag === "HttpClientError" }, }), ), }) }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(CustomFetchLive), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(CustomFetchLive)) } diff --git a/apps/web/src/lib/services/desktop/tauri-auth.ts b/apps/web/src/lib/services/desktop/tauri-auth.ts index 5385590d0..0ff3fb278 100644 --- a/apps/web/src/lib/services/desktop/tauri-auth.ts +++ b/apps/web/src/lib/services/desktop/tauri-auth.ts @@ -169,7 +169,7 @@ export class TauriAuth extends ServiceMap.Service()("TauriAuth", { ) }).pipe( Effect.timeout(Duration.minutes(2)), - Effect.catchTag("TimeoutException", () => + Effect.catchTag("TimeoutError", () => Effect.fail( new OAuthTimeoutError({ message: "OAuth callback timeout after 2 minutes", diff --git a/apps/web/src/lib/services/desktop/token-exchange.ts b/apps/web/src/lib/services/desktop/token-exchange.ts index 9613cb098..014a48ce1 100644 --- a/apps/web/src/lib/services/desktop/token-exchange.ts +++ b/apps/web/src/lib/services/desktop/token-exchange.ts @@ -81,14 +81,14 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc ) }).pipe( // Map HTTP client errors to TokenExchangeError - Effect.catchTag("TimeoutException", () => + Effect.catchTag("TimeoutError", () => Effect.fail( new TokenExchangeError({ message: "Token exchange timed out", }), ), ), - Effect.catchTag("RequestError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new TokenExchangeError({ message: "Network error during token exchange", @@ -96,7 +96,7 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc }), ), ), - Effect.catchTag("ResponseError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new TokenExchangeError({ message: "Server error during token exchange", @@ -144,14 +144,14 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc ) }).pipe( // Map HTTP client errors to TokenExchangeError - Effect.catchTag("TimeoutException", () => + Effect.catchTag("TimeoutError", () => Effect.fail( new TokenExchangeError({ message: "Token refresh timed out", }), ), ), - Effect.catchTag("RequestError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new TokenExchangeError({ message: "Network error during token refresh", @@ -159,7 +159,7 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc }), ), ), - Effect.catchTag("ResponseError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new TokenExchangeError({ message: "Server error during token refresh", @@ -171,9 +171,7 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(FetchHttpClient.layer), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(FetchHttpClient.layer)) /** * Mock token response for testing diff --git a/bots/hazel-bot/package.json b/bots/hazel-bot/package.json index 22ebe16b5..94cd1d4aa 100644 --- a/bots/hazel-bot/package.json +++ b/bots/hazel-bot/package.json @@ -9,8 +9,8 @@ "dev": "bun run --watch src/index.ts" }, "dependencies": { - "@hazel-chat/bot-sdk": "workspace:*", "@effect/ai-openrouter": "catalog:effect", + "@hazel-chat/bot-sdk": "workspace:*", "@hazel/domain": "workspace:*", "@hazel/integrations": "workspace:*", "@hazel/schema": "workspace:*", diff --git a/bots/hazel-bot/src/agent-loop.ts b/bots/hazel-bot/src/agent-loop.ts index 48325a79f..fabeefd63 100644 --- a/bots/hazel-bot/src/agent-loop.ts +++ b/bots/hazel-bot/src/agent-loop.ts @@ -66,9 +66,11 @@ export const streamAgentLoop = (options: { // Iteration timeout: wall-clock limit per LLM call Effect.timeoutOrElse({ onTimeout: () => - Effect.fail(new IterationTimeoutError({ - message: "Single LLM call exceeded 2 minute time limit", - })), + Effect.fail( + new IterationTimeoutError({ + message: "Single LLM call exceeded 2 minute time limit", + }), + ), duration: ITERATION_TIMEOUT, }), ) diff --git a/bots/hazel-bot/src/errors.ts b/bots/hazel-bot/src/errors.ts index ba4fde78b..875f51ca1 100644 --- a/bots/hazel-bot/src/errors.ts +++ b/bots/hazel-bot/src/errors.ts @@ -23,6 +23,9 @@ export class IterationTimeoutError extends Schema.TaggedErrorClass()("SessionTimeoutError", { - message: Schema.String, -}) {} +export class SessionTimeoutError extends Schema.TaggedErrorClass()( + "SessionTimeoutError", + { + message: Schema.String, + }, +) {} diff --git a/bots/hazel-bot/src/handler.ts b/bots/hazel-bot/src/handler.ts index 70baeca51..18cc25410 100644 --- a/bots/hazel-bot/src/handler.ts +++ b/bots/hazel-bot/src/handler.ts @@ -115,9 +115,11 @@ export const handleAIRequest = (params: { }).pipe( Effect.timeoutOrElse({ onTimeout: () => - Effect.fail(new SessionTimeoutError({ - message: "Overall AI session exceeded 3 minute time limit", - })), + Effect.fail( + new SessionTimeoutError({ + message: "Overall AI session exceeded 3 minute time limit", + }), + ), duration: Duration.minutes(3), }), ), diff --git a/bots/linear-bot/package.json b/bots/linear-bot/package.json index 81f15f5c8..53088e110 100644 --- a/bots/linear-bot/package.json +++ b/bots/linear-bot/package.json @@ -9,8 +9,8 @@ "dev": "bun run --watch src/index.ts" }, "dependencies": { - "@hazel-chat/bot-sdk": "workspace:*", "@effect/ai-openrouter": "catalog:effect", + "@hazel-chat/bot-sdk": "workspace:*", "@hazel/db": "workspace:*", "@hazel/domain": "workspace:*", "@hazel/integrations": "workspace:*", diff --git a/bun.lock b/bun.lock index bad4e2943..ff01049f5 100644 --- a/bun.lock +++ b/bun.lock @@ -362,17 +362,6 @@ "@types/bun": "1.3.9", }, }, - "libs/ai-openrouter": { - "name": "@hazel/ai-openrouter", - "version": "0.0.1", - "dependencies": { - "@effect/ai-openrouter": "catalog:effect", - "effect": "catalog:effect", - }, - "devDependencies": { - "typescript": "^5.9.3", - }, - }, "libs/bot-sdk": { "name": "@hazel-chat/bot-sdk", "version": "0.1.0", @@ -1046,8 +1035,6 @@ "@hazel/actors": ["@hazel/actors@workspace:packages/actors"], - "@hazel/ai-openrouter": ["@hazel/ai-openrouter@workspace:libs/ai-openrouter"], - "@hazel/auth": ["@hazel/auth@workspace:packages/auth"], "@hazel/backend": ["@hazel/backend@workspace:apps/backend"], diff --git a/libs/ai-openrouter/package.json b/libs/ai-openrouter/package.json deleted file mode 100644 index 08eeedfa7..000000000 --- a/libs/ai-openrouter/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@hazel/ai-openrouter", - "version": "0.0.1", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": "./src/index.ts", - "./*": "./src/*.ts" - }, - "scripts": { - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@effect/ai-openrouter": "catalog:effect", - "effect": "catalog:effect" - }, - "devDependencies": { - "typescript": "^5.9.3" - } -} diff --git a/libs/ai-openrouter/src/Generated.ts b/libs/ai-openrouter/src/Generated.ts deleted file mode 100644 index 9a2452c50..000000000 --- a/libs/ai-openrouter/src/Generated.ts +++ /dev/null @@ -1,6105 +0,0 @@ -/** - * @since 1.0.0 - */ -import type * as HttpClient from "effect/unstable/http/HttpClient" -import * as HttpClientError from "effect/unstable/http/HttpClientError" -import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" -import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" -import * as Data from "effect/Data" -import * as Effect from "effect/Effect" -import * as S from "effect/Schema" - -export class CacheControlEphemeral extends S.Class("CacheControlEphemeral")({ - type: S.Literal("ephemeral"), -}) {} - -export const OpenResponsesReasoningFormat = S.Literals(["unknown", - "openai-responses-v1", - "azure-openai-responses-v1", - "xai-responses-v1", - "anthropic-claude-v1", - "google-gemini-v1",]) - -export const OpenResponsesReasoningType = S.Literal("reasoning") - -export const ReasoningTextContentType = S.Literal("reasoning_text") - -export class ReasoningTextContent extends S.Class("ReasoningTextContent")({ - type: ReasoningTextContentType, - text: S.String, -}) {} - -export const ReasoningSummaryTextType = S.Literal("summary_text") - -export class ReasoningSummaryText extends S.Class("ReasoningSummaryText")({ - type: ReasoningSummaryTextType, - text: S.String, -}) {} - -export const OpenResponsesReasoningStatusEnum = S.Literal("in_progress") - -export class OpenResponsesReasoning extends S.Class("OpenResponsesReasoning")({ - signature: S.optional(S.NullOr(S.String)), - format: S.optional(S.NullOr(OpenResponsesReasoningFormat)), - type: OpenResponsesReasoningType, - id: S.String, - content: S.optional(S.NullOr(S.Array(ReasoningTextContent))), - summary: S.Array(ReasoningSummaryText), - encrypted_content: S.optional(S.NullOr(S.String)), - status: S.optional(S.NullOr(S.Union([ - OpenResponsesReasoningStatusEnum, - OpenResponsesReasoningStatusEnum, - OpenResponsesReasoningStatusEnum, - ]))), -}) {} - -export class ReasoningDetailSummary extends S.Class("ReasoningDetailSummary")({ - id: S.optional(S.NullOr(S.String)), - type: S.Literal("reasoning.summary"), - index: S.optional(S.Number), - format: S.optional(S.NullOr(OpenResponsesReasoningFormat)), - summary: S.String, -}) {} - -export class ReasoningDetailEncrypted extends S.Class("ReasoningDetailEncrypted")({ - id: S.optional(S.NullOr(S.String)), - type: S.Literal("reasoning.encrypted"), - index: S.optional(S.Number), - format: S.optional(S.NullOr(OpenResponsesReasoningFormat)), - data: S.String, -}) {} - -export class ReasoningDetailText extends S.Class("ReasoningDetailText")({ - id: S.optional(S.NullOr(S.String)), - type: S.Literal("reasoning.text"), - index: S.optional(S.Number), - format: S.optional(S.NullOr(OpenResponsesReasoningFormat)), - text: S.optional(S.NullOr(S.String)), - signature: S.optional(S.NullOr(S.String)), -}) {} - -export const ReasoningDetail = S.Union([ReasoningDetailSummary, - ReasoningDetailEncrypted, - ReasoningDetailText,]) - -export class FileAnnotationDetail extends S.Class("FileAnnotationDetail")({ - type: S.Literal("file"), - file: S.Struct({ - hash: S.String, - name: S.optional(S.NullOr(S.String)), - content: S.Array( - S.Union([ - S.Struct({ - type: S.Literal("text"), - text: S.String, - }), - S.Struct({ - type: S.Literal("image_url"), - image_url: S.Struct({ - url: S.String, - }), - }), - ]), - ), - }), -}) {} - -export class URLCitationAnnotationDetail extends S.Class( - "URLCitationAnnotationDetail", -)({ - type: S.Literal("url_citation"), - url_citation: S.Struct({ - end_index: S.Number, - start_index: S.Number, - title: S.String, - url: S.String, - content: S.optional(S.NullOr(S.String)), - }), -}) {} - -export const AnnotationDetail = S.Union([FileAnnotationDetail, URLCitationAnnotationDetail]) - -export const OpenResponsesEasyInputMessageType = S.Literal("message") - -export const OpenResponsesEasyInputMessageRoleEnum = S.Literal("developer") - -export const ResponseInputTextType = S.Literal("input_text") - -/** - * Text input content item - */ -export class ResponseInputText extends S.Class("ResponseInputText")({ - type: ResponseInputTextType, - text: S.String, -}) {} - -export const ResponseInputFileType = S.Literal("input_file") - -/** - * File input content item - */ -export class ResponseInputFile extends S.Class("ResponseInputFile")({ - type: ResponseInputFileType, - file_id: S.optional(S.NullOr(S.String)), - file_data: S.optional(S.NullOr(S.String)), - filename: S.optional(S.NullOr(S.String)), - file_url: S.optional(S.NullOr(S.String)), -}) {} - -export const ResponseInputAudioType = S.Literal("input_audio") - -export const ResponseInputAudioInputAudioFormat = S.Literals(["mp3", "wav"]) - -/** - * Audio input content item - */ -export class ResponseInputAudio extends S.Class("ResponseInputAudio")({ - type: ResponseInputAudioType, - input_audio: S.Struct({ - data: S.String, - format: ResponseInputAudioInputAudioFormat, - }), -}) {} - -export const ResponseInputVideoType = S.Literal("input_video") - -/** - * Video input content item - */ -export class ResponseInputVideo extends S.Class("ResponseInputVideo")({ - type: ResponseInputVideoType, - /** - * A base64 data URL or remote URL that resolves to a video file - */ - video_url: S.String, -}) {} - -export class OpenResponsesEasyInputMessage extends S.Class( - "OpenResponsesEasyInputMessage", -)({ - type: S.optional(S.NullOr(OpenResponsesEasyInputMessageType)), - role: S.Union([ - OpenResponsesEasyInputMessageRoleEnum, - OpenResponsesEasyInputMessageRoleEnum, - OpenResponsesEasyInputMessageRoleEnum, - OpenResponsesEasyInputMessageRoleEnum, - ]), - content: S.Union([ - S.Array( - S.Union([ - ResponseInputText, - /** - * Image input content item - */ - S.Struct({ - type: S.Literal("input_image"), - detail: S.Literals(["auto", "high", "low"]), - image_url: S.optional(S.NullOr(S.String)), - }), - ResponseInputFile, - ResponseInputAudio, - ResponseInputVideo, - ]), - ), - S.String, - ]), -}) {} - -export const OpenResponsesInputMessageItemType = S.Literal("message") - -export const OpenResponsesInputMessageItemRoleEnum = S.Literal("developer") - -export class OpenResponsesInputMessageItem extends S.Class( - "OpenResponsesInputMessageItem", -)({ - id: S.optional(S.NullOr(S.String)), - type: S.optional(S.NullOr(OpenResponsesInputMessageItemType)), - role: S.Union([ - OpenResponsesInputMessageItemRoleEnum, - OpenResponsesInputMessageItemRoleEnum, - OpenResponsesInputMessageItemRoleEnum, - ]), - content: S.Array( - S.Union([ - ResponseInputText, - /** - * Image input content item - */ - S.Struct({ - type: S.Literal("input_image"), - detail: S.Literals(["auto", "high", "low"]), - image_url: S.optional(S.NullOr(S.String)), - }), - ResponseInputFile, - ResponseInputAudio, - ResponseInputVideo, - ]), - ), -}) {} - -export const OpenResponsesFunctionToolCallType = S.Literal("function_call") - -export const ToolCallStatus = S.Literals(["in_progress", "completed", "incomplete"]) - -/** - * A function call initiated by the model - */ -export class OpenResponsesFunctionToolCall extends S.Class( - "OpenResponsesFunctionToolCall", -)({ - type: OpenResponsesFunctionToolCallType, - call_id: S.String, - name: S.String, - arguments: S.String, - id: S.String, - status: S.optional(S.NullOr(ToolCallStatus)), -}) {} - -export const OpenResponsesFunctionCallOutputType = S.Literal("function_call_output") - -/** - * The output from a function call execution - */ -export class OpenResponsesFunctionCallOutput extends S.Class( - "OpenResponsesFunctionCallOutput", -)({ - type: OpenResponsesFunctionCallOutputType, - id: S.optional(S.NullOr(S.String)), - call_id: S.String, - output: S.String, - status: S.optional(S.NullOr(ToolCallStatus)), -}) {} - -export const ResponsesOutputMessageRole = S.Literal("assistant") - -export const ResponsesOutputMessageType = S.Literal("message") - -export const ResponsesOutputMessageStatusEnum = S.Literal("in_progress") - -export const ResponseOutputTextType = S.Literal("output_text") - -export const FileCitationType = S.Literal("file_citation") - -export class FileCitation extends S.Class("FileCitation")({ - type: FileCitationType, - file_id: S.String, - filename: S.String, - index: S.Number, -}) {} - -export const URLCitationType = S.Literal("url_citation") - -export class URLCitation extends S.Class("URLCitation")({ - type: URLCitationType, - url: S.String, - title: S.String, - start_index: S.Number, - end_index: S.Number, -}) {} - -export const FilePathType = S.Literal("file_path") - -export class FilePath extends S.Class("FilePath")({ - type: FilePathType, - file_id: S.String, - index: S.Number, -}) {} - -export const OpenAIResponsesAnnotation = S.Union([FileCitation, URLCitation, FilePath]) - -export class ResponseOutputText extends S.Class("ResponseOutputText")({ - type: ResponseOutputTextType, - text: S.String, - annotations: S.optional(S.NullOr(S.Array(OpenAIResponsesAnnotation))), - logprobs: S.optional(S.NullOr(S.Array( - S.Struct({ - token: S.String, - bytes: S.Array(S.Number), - logprob: S.Number, - top_logprobs: S.Array( - S.Struct({ - token: S.String, - bytes: S.Array(S.Number), - logprob: S.Number, - }), - ), - }), - ))), -}) {} - -export const OpenAIResponsesRefusalContentType = S.Literal("refusal") - -export class OpenAIResponsesRefusalContent extends S.Class( - "OpenAIResponsesRefusalContent", -)({ - type: OpenAIResponsesRefusalContentType, - refusal: S.String, -}) {} - -export class ResponsesOutputMessage extends S.Class("ResponsesOutputMessage")({ - id: S.String, - role: ResponsesOutputMessageRole, - type: ResponsesOutputMessageType, - status: S.optional(S.NullOr(S.Union([ - ResponsesOutputMessageStatusEnum, - ResponsesOutputMessageStatusEnum, - ResponsesOutputMessageStatusEnum, - ]))), - content: S.Array(S.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), -}) {} - -/** - * The format of the reasoning content - */ -export const ResponsesOutputItemReasoningFormat = S.Literals(["unknown", - "openai-responses-v1", - "azure-openai-responses-v1", - "xai-responses-v1", - "anthropic-claude-v1", - "google-gemini-v1",]) - -export const ResponsesOutputItemReasoningType = S.Literal("reasoning") - -export const ResponsesOutputItemReasoningStatusEnum = S.Literal("in_progress") - -export class ResponsesOutputItemReasoning extends S.Class( - "ResponsesOutputItemReasoning", -)({ - /** - * A signature for the reasoning content, used for verification - */ - signature: S.optional(S.NullOr(S.String)), - /** - * The format of the reasoning content - */ - format: S.optional(S.NullOr(ResponsesOutputItemReasoningFormat)), - type: ResponsesOutputItemReasoningType, - id: S.String, - content: S.optional(S.NullOr(S.Array(ReasoningTextContent))), - summary: S.Array(ReasoningSummaryText), - encrypted_content: S.optional(S.NullOr(S.String)), - status: S.optional(S.NullOr(S.Union([ - ResponsesOutputItemReasoningStatusEnum, - ResponsesOutputItemReasoningStatusEnum, - ResponsesOutputItemReasoningStatusEnum, - ]))), -}) {} - -export const ResponsesOutputItemFunctionCallType = S.Literal("function_call") - -export const ResponsesOutputItemFunctionCallStatusEnum = S.Literal("in_progress") - -export class ResponsesOutputItemFunctionCall extends S.Class( - "ResponsesOutputItemFunctionCall", -)({ - type: ResponsesOutputItemFunctionCallType, - id: S.optional(S.NullOr(S.String)), - name: S.String, - arguments: S.String, - call_id: S.String, - status: S.optional(S.NullOr(S.Union([ - ResponsesOutputItemFunctionCallStatusEnum, - ResponsesOutputItemFunctionCallStatusEnum, - ResponsesOutputItemFunctionCallStatusEnum, - ]))), -}) {} - -export const ResponsesWebSearchCallOutputType = S.Literal("web_search_call") - -export const WebSearchStatus = S.Literals(["completed", "searching", "in_progress", "failed"]) - -export class ResponsesWebSearchCallOutput extends S.Class( - "ResponsesWebSearchCallOutput", -)({ - type: ResponsesWebSearchCallOutputType, - id: S.String, - status: WebSearchStatus, -}) {} - -export const ResponsesOutputItemFileSearchCallType = S.Literal("file_search_call") - -export class ResponsesOutputItemFileSearchCall extends S.Class( - "ResponsesOutputItemFileSearchCall", -)({ - type: ResponsesOutputItemFileSearchCallType, - id: S.String, - queries: S.Array(S.String), - status: WebSearchStatus, -}) {} - -export const ResponsesImageGenerationCallType = S.Literal("image_generation_call") - -export const ImageGenerationStatus = S.Literals(["in_progress", "completed", "generating", "failed"]) - -export class ResponsesImageGenerationCall extends S.Class( - "ResponsesImageGenerationCall", -)({ - type: ResponsesImageGenerationCallType, - id: S.String, - result: S.NullOr(S.String).pipe(S.optional, S.withDecodingDefault(() => null)), - status: ImageGenerationStatus, -}) {} - -/** - * Input for a response request - can be a string or array of items - */ -export const OpenResponsesInput = S.Union([S.String, - S.Array( - S.Union([ - OpenResponsesReasoning, - OpenResponsesEasyInputMessage, - OpenResponsesInputMessageItem, - OpenResponsesFunctionToolCall, - OpenResponsesFunctionCallOutput, - ResponsesOutputMessage, - ResponsesOutputItemReasoning, - ResponsesOutputItemFunctionCall, - ResponsesWebSearchCallOutput, - ResponsesOutputItemFileSearchCall, - ResponsesImageGenerationCall, - ]), - ),]) - -/** - * Metadata key-value pairs for the request. Keys must be ≤64 characters and cannot contain brackets. Values must be ≤512 characters. Maximum 16 pairs allowed. - */ -export const OpenResponsesRequestMetadata = S.Record(S.String, S.Unknown) - -export const OpenResponsesWebSearchPreviewToolType = S.Literal("web_search_preview") - -/** - * Size of the search context for web search tools - */ -export const ResponsesSearchContextSize = S.Literals(["low", "medium", "high"]) - -export const WebSearchPreviewToolUserLocationType = S.Literal("approximate") - -export class WebSearchPreviewToolUserLocation extends S.Class( - "WebSearchPreviewToolUserLocation", -)({ - type: WebSearchPreviewToolUserLocationType, - city: S.optional(S.NullOr(S.String)), - country: S.optional(S.NullOr(S.String)), - region: S.optional(S.NullOr(S.String)), - timezone: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Web search preview tool configuration - */ -export class OpenResponsesWebSearchPreviewTool extends S.Class( - "OpenResponsesWebSearchPreviewTool", -)({ - type: OpenResponsesWebSearchPreviewToolType, - search_context_size: S.optional(S.NullOr(ResponsesSearchContextSize)), - user_location: S.optional(S.NullOr(WebSearchPreviewToolUserLocation)), -}) {} - -export const OpenResponsesWebSearchPreview20250311ToolType = S.Literals(["web_search_preview_2025_03_11",]) - -/** - * Web search preview tool configuration (2025-03-11 version) - */ -export class OpenResponsesWebSearchPreview20250311Tool extends S.Class( - "OpenResponsesWebSearchPreview20250311Tool", -)({ - type: OpenResponsesWebSearchPreview20250311ToolType, - search_context_size: S.optional(S.NullOr(ResponsesSearchContextSize)), - user_location: S.optional(S.NullOr(WebSearchPreviewToolUserLocation)), -}) {} - -export const OpenResponsesWebSearchToolType = S.Literal("web_search") - -export const ResponsesWebSearchUserLocationType = S.Literal("approximate") - -/** - * User location information for web search - */ -export class ResponsesWebSearchUserLocation extends S.Class( - "ResponsesWebSearchUserLocation", -)({ - type: S.optional(S.NullOr(ResponsesWebSearchUserLocationType)), - city: S.optional(S.NullOr(S.String)), - country: S.optional(S.NullOr(S.String)), - region: S.optional(S.NullOr(S.String)), - timezone: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Web search tool configuration - */ -export class OpenResponsesWebSearchTool extends S.Class( - "OpenResponsesWebSearchTool", -)({ - type: OpenResponsesWebSearchToolType, - filters: S.optional(S.NullOr(S.Struct({ - allowed_domains: S.optional(S.NullOr(S.Array(S.String))), - }))), - search_context_size: S.optional(S.NullOr(ResponsesSearchContextSize)), - user_location: S.optional(S.NullOr(ResponsesWebSearchUserLocation)), -}) {} - -export const OpenResponsesWebSearch20250826ToolType = S.Literal("web_search_2025_08_26") - -/** - * Web search tool configuration (2025-08-26 version) - */ -export class OpenResponsesWebSearch20250826Tool extends S.Class( - "OpenResponsesWebSearch20250826Tool", -)({ - type: OpenResponsesWebSearch20250826ToolType, - filters: S.optional(S.NullOr(S.Struct({ - allowed_domains: S.optional(S.NullOr(S.Array(S.String))), - }))), - search_context_size: S.optional(S.NullOr(ResponsesSearchContextSize)), - user_location: S.optional(S.NullOr(ResponsesWebSearchUserLocation)), -}) {} - -export const OpenAIResponsesToolChoiceEnum = S.Literal("required") - -export const OpenAIResponsesToolChoiceEnumType = S.Literal("function") - -export const OpenAIResponsesToolChoiceEnumTypeEnum = S.Literal("web_search_preview") - -export const OpenAIResponsesToolChoice = S.Union([OpenAIResponsesToolChoiceEnum, - OpenAIResponsesToolChoiceEnum, - OpenAIResponsesToolChoiceEnum, - S.Struct({ - type: OpenAIResponsesToolChoiceEnumType, - name: S.String, - }), - S.Struct({ - type: S.Union([OpenAIResponsesToolChoiceEnumTypeEnum, OpenAIResponsesToolChoiceEnumTypeEnum]), - }),]) - -export const ResponsesFormatTextType = S.Literal("text") - -/** - * Plain text response format - */ -export class ResponsesFormatText extends S.Class("ResponsesFormatText")({ - type: ResponsesFormatTextType, -}) {} - -export const ResponsesFormatJSONObjectType = S.Literal("json_object") - -/** - * JSON object response format - */ -export class ResponsesFormatJSONObject extends S.Class( - "ResponsesFormatJSONObject", -)({ - type: ResponsesFormatJSONObjectType, -}) {} - -export const ResponsesFormatTextJSONSchemaConfigType = S.Literal("json_schema") - -/** - * JSON schema constrained response format - */ -export class ResponsesFormatTextJSONSchemaConfig extends S.Class( - "ResponsesFormatTextJSONSchemaConfig", -)({ - type: ResponsesFormatTextJSONSchemaConfigType, - name: S.String, - description: S.optional(S.NullOr(S.String)), - strict: S.optional(S.NullOr(S.Boolean)), - schema: S.Record(S.String, S.Unknown), -}) {} - -/** - * Text response format configuration - */ -export const ResponseFormatTextConfig = S.Union([ResponsesFormatText, - ResponsesFormatJSONObject, - ResponsesFormatTextJSONSchemaConfig,]) - -export const OpenResponsesResponseTextVerbosity = S.Literals(["high", "low", "medium"]) - -/** - * Text output configuration including format and verbosity - */ -export class OpenResponsesResponseText extends S.Class( - "OpenResponsesResponseText", -)({ - format: S.optional(S.NullOr(ResponseFormatTextConfig)), - verbosity: S.optional(S.NullOr(OpenResponsesResponseTextVerbosity)), -}) {} - -export const OpenAIResponsesReasoningEffort = S.Literals(["xhigh", - "high", - "medium", - "low", - "minimal", - "none",]) - -export const ReasoningSummaryVerbosity = S.Literals(["auto", "concise", "detailed"]) - -export class OpenResponsesReasoningConfig extends S.Class( - "OpenResponsesReasoningConfig", -)({ - max_tokens: S.optional(S.NullOr(S.Number)), - enabled: S.optional(S.NullOr(S.Boolean)), - effort: S.optional(S.NullOr(OpenAIResponsesReasoningEffort)), - summary: S.optional(S.NullOr(ReasoningSummaryVerbosity)), -}) {} - -export const ResponsesOutputModality = S.Literals(["text", "image"]) - -export class OpenAIResponsesPrompt extends S.Class("OpenAIResponsesPrompt")({ - id: S.String, - variables: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -export const OpenAIResponsesIncludable = S.Literals(["file_search_call.results", - "message.input_image.image_url", - "computer_call_output.output.image_url", - "reasoning.encrypted_content", - "code_interpreter_call.outputs",]) - -export const OpenResponsesRequestServiceTier = S.Literal("auto") - -export const OpenResponsesRequestTruncationEnum = S.Literals(["auto", "disabled"]) - -export const OpenResponsesRequestTruncation = OpenResponsesRequestTruncationEnum - -/** - * Data collection setting. If no available model provider meets the requirement, your request will return an error. - * - allow: (default) allow providers which store user data non-transiently and may train on it - * - * - deny: use only providers which do not collect user data. - */ -export const DataCollection = S.Literals(["deny", "allow"]) - -export const ProviderName = S.Literals(["AI21", - "AionLabs", - "Alibaba", - "Amazon Bedrock", - "Amazon Nova", - "Anthropic", - "Arcee AI", - "AtlasCloud", - "Avian", - "Azure", - "BaseTen", - "BytePlus", - "Black Forest Labs", - "Cerebras", - "Chutes", - "Cirrascale", - "Clarifai", - "Cloudflare", - "Cohere", - "Crusoe", - "DeepInfra", - "DeepSeek", - "Featherless", - "Fireworks", - "Friendli", - "GMICloud", - "Google", - "Google AI Studio", - "Groq", - "Hyperbolic", - "Inception", - "Inceptron", - "InferenceNet", - "Infermatic", - "Inflection", - "Liquid", - "Mara", - "Mancer 2", - "Minimax", - "ModelRun", - "Mistral", - "Modular", - "Moonshot AI", - "Morph", - "NCompass", - "Nebius", - "NextBit", - "Novita", - "Nvidia", - "OpenAI", - "OpenInference", - "Parasail", - "Perplexity", - "Phala", - "Relace", - "SambaNova", - "Seed", - "SiliconFlow", - "Sourceful", - "Stealth", - "StreamLake", - "Switchpoint", - "Together", - "Upstage", - "Venice", - "WandB", - "Xiaomi", - "xAI", - "Z.AI", - "FakeProvider",]) - -export const Quantization = S.Literals(["int4", - "int8", - "fp4", - "fp6", - "fp8", - "fp16", - "bf16", - "fp32", - "unknown",]) - -export const ProviderSort = S.Literals(["price", "throughput", "latency"]) - -export const ProviderSortConfigPartitionEnum = S.Literals(["model", "none"]) - -export class ProviderSortConfig extends S.Class("ProviderSortConfig")({ - by: S.optional(S.NullOr(ProviderSort)), - partition: S.optional(S.NullOr(ProviderSortConfigPartitionEnum)), -}) {} - -/** - * A value in string format that is a large number - */ -export const BigNumberUnion = S.String - -/** - * Percentile-based throughput cutoffs. All specified cutoffs must be met for an endpoint to be preferred. - */ -export class PercentileThroughputCutoffs extends S.Class( - "PercentileThroughputCutoffs", -)({ - /** - * Minimum p50 throughput (tokens/sec) - */ - p50: S.optional(S.NullOr(S.Number)), - /** - * Minimum p75 throughput (tokens/sec) - */ - p75: S.optional(S.NullOr(S.Number)), - /** - * Minimum p90 throughput (tokens/sec) - */ - p90: S.optional(S.NullOr(S.Number)), - /** - * Minimum p99 throughput (tokens/sec) - */ - p99: S.optional(S.NullOr(S.Number)), -}) {} - -/** - * Preferred minimum throughput (in tokens per second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints below the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. - */ -export const PreferredMinThroughput = S.Union([S.Number, PercentileThroughputCutoffs]) - -/** - * Percentile-based latency cutoffs. All specified cutoffs must be met for an endpoint to be preferred. - */ -export class PercentileLatencyCutoffs extends S.Class("PercentileLatencyCutoffs")({ - /** - * Maximum p50 latency (seconds) - */ - p50: S.optional(S.NullOr(S.Number)), - /** - * Maximum p75 latency (seconds) - */ - p75: S.optional(S.NullOr(S.Number)), - /** - * Maximum p90 latency (seconds) - */ - p90: S.optional(S.NullOr(S.Number)), - /** - * Maximum p99 latency (seconds) - */ - p99: S.optional(S.NullOr(S.Number)), -}) {} - -/** - * Preferred maximum latency (in seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints above the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. - */ -export const PreferredMaxLatency = S.Union([S.Number, PercentileLatencyCutoffs]) - -/** - * The search engine to use for web search. - */ -export const WebSearchEngine = S.Literals(["native", "exa"]) - -/** - * The engine to use for parsing PDF files. - */ -export const PDFParserEngine = S.Literals(["mistral-ocr", "pdf-text", "native"]) - -/** - * Options for PDF parsing. - */ -export class PDFParserOptions extends S.Class("PDFParserOptions")({ - engine: S.optional(S.NullOr(PDFParserEngine)), -}) {} - -/** - * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). - */ -export const OpenResponsesRequestRoute = S.Literals(["fallback", "sort"]) - -/** - * Request schema for Responses endpoint - */ -export class OpenResponsesRequest extends S.Class("OpenResponsesRequest")({ - input: S.optional(S.NullOr(OpenResponsesInput)), - instructions: S.optional(S.NullOr(S.String)), - metadata: S.optional(S.NullOr(OpenResponsesRequestMetadata)), - tools: S.optional(S.NullOr(S.Array( - S.Union([ - /** - * Function tool definition - */ - S.Struct({}), - OpenResponsesWebSearchPreviewTool, - OpenResponsesWebSearchPreview20250311Tool, - OpenResponsesWebSearchTool, - OpenResponsesWebSearch20250826Tool, - ]), - ))), - tool_choice: S.optional(S.NullOr(OpenAIResponsesToolChoice)), - parallel_tool_calls: S.optional(S.NullOr(S.Boolean)), - model: S.optional(S.NullOr(S.String)), - models: S.optional(S.NullOr(S.Array(S.String))), - text: S.optional(S.NullOr(OpenResponsesResponseText)), - reasoning: S.optional(S.NullOr(OpenResponsesReasoningConfig)), - max_output_tokens: S.optional(S.NullOr(S.Number)), - temperature: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(2)))), - top_p: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0)))), - top_logprobs: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(20)))), - max_tool_calls: S.optional(S.NullOr(S.Int)), - presence_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), - frequency_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), - top_k: S.optional(S.NullOr(S.Number)), - /** - * Provider-specific image configuration options. Keys and values vary by model/provider. See https://openrouter.ai/docs/features/multimodal/image-generation for more details. - */ - image_config: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), - /** - * Output modalities for the response. Supported values are "text" and "image". - */ - modalities: S.optional(S.NullOr(S.Array(ResponsesOutputModality))), - prompt_cache_key: S.optional(S.NullOr(S.String)), - previous_response_id: S.optional(S.NullOr(S.String)), - prompt: S.optional(S.NullOr(OpenAIResponsesPrompt)), - include: S.optional(S.NullOr(S.Array(OpenAIResponsesIncludable))), - background: S.optional(S.NullOr(S.Boolean)), - safety_identifier: S.optional(S.NullOr(S.String)), - store: S.NullOr(S.Literal(false)).pipe(S.optional, S.withDecodingDefault(() => false as const)), - service_tier: S.NullOr(OpenResponsesRequestServiceTier).pipe(S.optional, S.withDecodingDefault(() => "auto" as const)), - truncation: S.optional(S.NullOr(OpenResponsesRequestTruncation)), - stream: S.NullOr(S.Boolean).pipe(S.optional, S.withDecodingDefault(() => false as const)), - /** - * When multiple model providers are available, optionally indicate your routing preference. - */ - provider: S.optional(S.NullOr(S.Struct({ - /** - * Whether to allow backup providers to serve requests - * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. - * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. - */ - allow_fallbacks: S.optional(S.NullOr(S.Boolean)), - /** - * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. - */ - require_parameters: S.optional(S.NullOr(S.Boolean)), - data_collection: S.optional(S.NullOr(DataCollection)), - /** - * Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used. - */ - zdr: S.optional(S.NullOr(S.Boolean)), - /** - * Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used. - */ - enforce_distillable_text: S.optional(S.NullOr(S.Boolean)), - /** - * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. - */ - order: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), - /** - * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. - */ - only: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), - /** - * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. - */ - ignore: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), - /** - * A list of quantization levels to filter the provider by. - */ - quantizations: S.optional(S.NullOr(S.Array(Quantization))), - /** - * The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. - */ - sort: S.optional(S.NullOr(S.Union([ProviderSort, ProviderSortConfig]))), - /** - * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. - */ - max_price: S.optional(S.Struct({ - prompt: S.optional(S.NullOr(BigNumberUnion)), - completion: S.optional(S.NullOr(BigNumberUnion)), - image: S.optional(S.NullOr(BigNumberUnion)), - audio: S.optional(S.NullOr(BigNumberUnion)), - request: S.optional(S.NullOr(BigNumberUnion)), - })), - preferred_min_throughput: S.optional(S.NullOr(PreferredMinThroughput)), - preferred_max_latency: S.optional(S.NullOr(PreferredMaxLatency)), - }))), - /** - * Plugins you want to enable for this request, including their settings. - */ - plugins: S.optional(S.NullOr(S.Array( - S.Union([ - S.Struct({ - id: S.Literal("auto-router"), - /** - * Set to false to disable the auto-router plugin for this request. Defaults to true. - */ - enabled: S.optional(S.NullOr(S.Boolean)), - /** - * List of model patterns to filter which models the auto-router can route between. Supports wildcards (e.g., "anthropic/*" matches all Anthropic models). When not specified, uses the default supported models list. - */ - allowed_models: S.optional(S.NullOr(S.Array(S.String))), - }), - S.Struct({ - id: S.Literal("moderation"), - }), - S.Struct({ - id: S.Literal("web"), - /** - * Set to false to disable the web-search plugin for this request. Defaults to true. - */ - enabled: S.optional(S.NullOr(S.Boolean)), - max_results: S.optional(S.NullOr(S.Number)), - search_prompt: S.optional(S.NullOr(S.String)), - engine: S.optional(S.NullOr(WebSearchEngine)), - }), - S.Struct({ - id: S.Literal("file-parser"), - /** - * Set to false to disable the file-parser plugin for this request. Defaults to true. - */ - enabled: S.optional(S.NullOr(S.Boolean)), - pdf: S.optional(S.NullOr(PDFParserOptions)), - }), - S.Struct({ - id: S.Literal("response-healing"), - /** - * Set to false to disable the response-healing plugin for this request. Defaults to true. - */ - enabled: S.optional(S.NullOr(S.Boolean)), - }), - ]), - ))), - /** - * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). - */ - route: S.optional(S.NullOr(OpenResponsesRequestRoute)), - /** - * A unique identifier representing your end-user, which helps distinguish between different users of your app. This allows your app to identify specific users in case of abuse reports, preventing your entire app from being affected by the actions of individual users. Maximum of 128 characters. - */ - user: S.optional(S.NullOr(S.String.check(S.isMaxLength(128)))), - /** - * A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters. - */ - session_id: S.optional(S.NullOr(S.String.check(S.isMaxLength(128)))), -}) {} - -export const OutputMessageRole = S.Literal("assistant") - -export const OutputMessageType = S.Literal("message") - -export const OutputMessageStatusEnum = S.Literal("in_progress") - -export class OutputMessage extends S.Class("OutputMessage")({ - id: S.String, - role: OutputMessageRole, - type: OutputMessageType, - status: S.optional(S.NullOr(S.Union([OutputMessageStatusEnum, OutputMessageStatusEnum, OutputMessageStatusEnum]))), - content: S.Array(S.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), -}) {} - -export const OutputItemReasoningType = S.Literal("reasoning") - -export const OutputItemReasoningStatusEnum = S.Literal("in_progress") - -export class OutputItemReasoning extends S.Class("OutputItemReasoning")({ - type: OutputItemReasoningType, - id: S.String, - content: S.optional(S.NullOr(S.Array(ReasoningTextContent))), - summary: S.Array(ReasoningSummaryText), - encrypted_content: S.optional(S.NullOr(S.String)), - status: S.optional(S.NullOr(S.Union([OutputItemReasoningStatusEnum, OutputItemReasoningStatusEnum, OutputItemReasoningStatusEnum]))), -}) {} - -export const OutputItemFunctionCallType = S.Literal("function_call") - -export const OutputItemFunctionCallStatusEnum = S.Literal("in_progress") - -export class OutputItemFunctionCall extends S.Class("OutputItemFunctionCall")({ - type: OutputItemFunctionCallType, - id: S.optional(S.NullOr(S.String)), - name: S.String, - arguments: S.String, - call_id: S.String, - status: S.optional(S.NullOr(S.Union([ - OutputItemFunctionCallStatusEnum, - OutputItemFunctionCallStatusEnum, - OutputItemFunctionCallStatusEnum, - ]))), -}) {} - -export const OutputItemWebSearchCallType = S.Literal("web_search_call") - -export class OutputItemWebSearchCall extends S.Class("OutputItemWebSearchCall")({ - type: OutputItemWebSearchCallType, - id: S.String, - status: WebSearchStatus, -}) {} - -export const OutputItemFileSearchCallType = S.Literal("file_search_call") - -export class OutputItemFileSearchCall extends S.Class("OutputItemFileSearchCall")({ - type: OutputItemFileSearchCallType, - id: S.String, - queries: S.Array(S.String), - status: WebSearchStatus, -}) {} - -export const OutputItemImageGenerationCallType = S.Literal("image_generation_call") - -export class OutputItemImageGenerationCall extends S.Class( - "OutputItemImageGenerationCall", -)({ - type: OutputItemImageGenerationCallType, - id: S.String, - result: S.NullOr(S.String).pipe(S.optional, S.withDecodingDefault(() => null)), - status: ImageGenerationStatus, -}) {} - -export class OpenAIResponsesUsage extends S.Class("OpenAIResponsesUsage")({ - input_tokens: S.Number, - input_tokens_details: S.Struct({ - cached_tokens: S.Number, - }), - output_tokens: S.Number, - output_tokens_details: S.Struct({ - reasoning_tokens: S.Number, - }), - total_tokens: S.Number, -}) {} - -export const OpenResponsesNonStreamingResponseObject = S.Literal("response") - -export const OpenAIResponsesResponseStatus = S.Literals(["completed", - "incomplete", - "in_progress", - "failed", - "cancelled", - "queued",]) - -export const ResponsesErrorFieldCode = S.Literals(["server_error", - "rate_limit_exceeded", - "invalid_prompt", - "vector_store_timeout", - "invalid_image", - "invalid_image_format", - "invalid_base64_image", - "invalid_image_url", - "image_too_large", - "image_too_small", - "image_parse_error", - "image_content_policy_violation", - "invalid_image_mode", - "image_file_too_large", - "unsupported_image_media_type", - "empty_image_file", - "failed_to_download_image", - "image_file_not_found",]) - -/** - * Error information returned from the API - */ -export class ResponsesErrorField extends S.Class("ResponsesErrorField")({ - code: ResponsesErrorFieldCode, - message: S.String, -}) {} - -export const OpenAIResponsesIncompleteDetailsReason = S.Literals(["max_output_tokens", - "content_filter",]) - -export class OpenAIResponsesIncompleteDetails extends S.Class( - "OpenAIResponsesIncompleteDetails", -)({ - reason: S.optional(S.NullOr(OpenAIResponsesIncompleteDetailsReason)), -}) {} - -export const ResponseInputImageType = S.Literal("input_image") - -export const ResponseInputImageDetail = S.Literals(["auto", "high", "low"]) - -/** - * Image input content item - */ -export class ResponseInputImage extends S.Class("ResponseInputImage")({ - type: ResponseInputImageType, - detail: ResponseInputImageDetail, - image_url: S.optional(S.NullOr(S.String)), -}) {} - -export const OpenAIResponsesInput = S.Union([S.String, - S.Array( - S.Union([ - S.Struct({ - type: S.optional(S.NullOr(S.Literal("message"))), - role: S.Union([ - S.Literal("user"), - S.Literal("system"), - S.Literal("assistant"), - S.Literal("developer"), - ]), - content: S.Union([ - S.Array( - S.Union([ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio]), - ), - S.String, - ]), - }), - S.Struct({ - id: S.String, - type: S.optional(S.NullOr(S.Literal("message"))), - role: S.Union([S.Literal("user"), S.Literal("system"), S.Literal("developer")]), - content: S.Array( - S.Union([ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio]), - ), - }), - S.Struct({ - type: S.Literal("function_call_output"), - id: S.optional(S.NullOr(S.String)), - call_id: S.String, - output: S.String, - status: S.optional(S.NullOr(ToolCallStatus)), - }), - S.Struct({ - type: S.Literal("function_call"), - call_id: S.String, - name: S.String, - arguments: S.String, - id: S.optional(S.NullOr(S.String)), - status: S.optional(S.NullOr(ToolCallStatus)), - }), - OutputItemImageGenerationCall, - OutputMessage, - ]), - ),]) - -export class OpenAIResponsesReasoningConfig extends S.Class( - "OpenAIResponsesReasoningConfig", -)({ - effort: S.optional(S.NullOr(OpenAIResponsesReasoningEffort)), - summary: S.optional(S.NullOr(ReasoningSummaryVerbosity)), -}) {} - -export const OpenAIResponsesServiceTier = S.Literals(["auto", "default", "flex", "priority", "scale"]) - -export const OpenAIResponsesTruncation = S.Literals(["auto", "disabled"]) - -export const ResponseTextConfigVerbosity = S.Literals(["high", "low", "medium"]) - -/** - * Text output configuration including format and verbosity - */ -export class ResponseTextConfig extends S.Class("ResponseTextConfig")({ - format: S.optional(S.NullOr(ResponseFormatTextConfig)), - verbosity: S.optional(S.NullOr(ResponseTextConfigVerbosity)), -}) {} - -export class OpenResponsesNonStreamingResponse extends S.Class( - "OpenResponsesNonStreamingResponse", -)({ - output: S.Array( - S.Union([ - OutputMessage, - OutputItemReasoning, - OutputItemFunctionCall, - OutputItemWebSearchCall, - OutputItemFileSearchCall, - OutputItemImageGenerationCall, - ]), - ), - usage: S.optional(S.NullOr(OpenAIResponsesUsage)), - id: S.String, - object: OpenResponsesNonStreamingResponseObject, - created_at: S.Number, - model: S.String, - status: OpenAIResponsesResponseStatus, - completed_at: S.NullOr(S.Number), - user: S.optional(S.NullOr(S.String)), - output_text: S.optional(S.NullOr(S.String)), - prompt_cache_key: S.optional(S.NullOr(S.String)), - safety_identifier: S.optional(S.NullOr(S.String)), - error: S.NullOr(ResponsesErrorField), - incomplete_details: S.NullOr(OpenAIResponsesIncompleteDetails), - max_tool_calls: S.optional(S.NullOr(S.Number)), - top_logprobs: S.optional(S.NullOr(S.Number)), - max_output_tokens: S.optional(S.NullOr(S.Number)), - temperature: S.NullOr(S.Number), - top_p: S.NullOr(S.Number), - presence_penalty: S.NullOr(S.Number), - frequency_penalty: S.NullOr(S.Number), - instructions: OpenAIResponsesInput, - metadata: S.NullOr(OpenResponsesRequestMetadata), - tools: S.Array( - S.Union([ - /** - * Function tool definition - */ - S.Struct({}), - OpenResponsesWebSearchPreviewTool, - OpenResponsesWebSearchPreview20250311Tool, - OpenResponsesWebSearchTool, - OpenResponsesWebSearch20250826Tool, - ]), - ), - tool_choice: OpenAIResponsesToolChoice, - parallel_tool_calls: S.Boolean, - prompt: S.optional(S.NullOr(OpenAIResponsesPrompt)), - background: S.optional(S.NullOr(S.Boolean)), - previous_response_id: S.optional(S.NullOr(S.String)), - reasoning: S.optional(S.NullOr(OpenAIResponsesReasoningConfig)), - service_tier: S.optional(S.NullOr(OpenAIResponsesServiceTier)), - store: S.optional(S.NullOr(S.Boolean)), - truncation: S.optional(S.NullOr(OpenAIResponsesTruncation)), - text: S.optional(S.NullOr(ResponseTextConfig)), -}) {} - -/** - * Error data for BadRequestResponse - */ -export class BadRequestResponseErrorData extends S.Class( - "BadRequestResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Bad Request - Invalid request parameters or malformed input - */ -export class BadRequestResponse extends S.Class("BadRequestResponse")({ - error: BadRequestResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Error data for UnauthorizedResponse - */ -export class UnauthorizedResponseErrorData extends S.Class( - "UnauthorizedResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Unauthorized - Authentication required or invalid credentials - */ -export class UnauthorizedResponse extends S.Class("UnauthorizedResponse")({ - error: UnauthorizedResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Error data for PaymentRequiredResponse - */ -export class PaymentRequiredResponseErrorData extends S.Class( - "PaymentRequiredResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Payment Required - Insufficient credits or quota to complete request - */ -export class PaymentRequiredResponse extends S.Class("PaymentRequiredResponse")({ - error: PaymentRequiredResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Error data for NotFoundResponse - */ -export class NotFoundResponseErrorData extends S.Class( - "NotFoundResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Not Found - Resource does not exist - */ -export class NotFoundResponse extends S.Class("NotFoundResponse")({ - error: NotFoundResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Error data for RequestTimeoutResponse - */ -export class RequestTimeoutResponseErrorData extends S.Class( - "RequestTimeoutResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Request Timeout - Operation exceeded time limit - */ -export class RequestTimeoutResponse extends S.Class("RequestTimeoutResponse")({ - error: RequestTimeoutResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Error data for PayloadTooLargeResponse - */ -export class PayloadTooLargeResponseErrorData extends S.Class( - "PayloadTooLargeResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Payload Too Large - Request payload exceeds size limits - */ -export class PayloadTooLargeResponse extends S.Class("PayloadTooLargeResponse")({ - error: PayloadTooLargeResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Error data for UnprocessableEntityResponse - */ -export class UnprocessableEntityResponseErrorData extends S.Class( - "UnprocessableEntityResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Unprocessable Entity - Semantic validation failure - */ -export class UnprocessableEntityResponse extends S.Class( - "UnprocessableEntityResponse", -)({ - error: UnprocessableEntityResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Error data for TooManyRequestsResponse - */ -export class TooManyRequestsResponseErrorData extends S.Class( - "TooManyRequestsResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Too Many Requests - Rate limit exceeded - */ -export class TooManyRequestsResponse extends S.Class("TooManyRequestsResponse")({ - error: TooManyRequestsResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Error data for InternalServerResponse - */ -export class InternalServerResponseErrorData extends S.Class( - "InternalServerResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Internal Server Error - Unexpected server error - */ -export class InternalServerResponse extends S.Class("InternalServerResponse")({ - error: InternalServerResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Error data for BadGatewayResponse - */ -export class BadGatewayResponseErrorData extends S.Class( - "BadGatewayResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Bad Gateway - Provider/upstream API failure - */ -export class BadGatewayResponse extends S.Class("BadGatewayResponse")({ - error: BadGatewayResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Error data for ServiceUnavailableResponse - */ -export class ServiceUnavailableResponseErrorData extends S.Class( - "ServiceUnavailableResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Service Unavailable - Service temporarily unavailable - */ -export class ServiceUnavailableResponse extends S.Class( - "ServiceUnavailableResponse", -)({ - error: ServiceUnavailableResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Error data for EdgeNetworkTimeoutResponse - */ -export class EdgeNetworkTimeoutResponseErrorData extends S.Class( - "EdgeNetworkTimeoutResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Infrastructure Timeout - Provider request timed out at edge network - */ -export class EdgeNetworkTimeoutResponse extends S.Class( - "EdgeNetworkTimeoutResponse", -)({ - error: EdgeNetworkTimeoutResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Error data for ProviderOverloadedResponse - */ -export class ProviderOverloadedResponseErrorData extends S.Class( - "ProviderOverloadedResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Provider Overloaded - Provider is temporarily overloaded - */ -export class ProviderOverloadedResponse extends S.Class( - "ProviderOverloadedResponse", -)({ - error: ProviderOverloadedResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -export const OpenRouterAnthropicMessageParamRole = S.Literals(["user", "assistant"]) - -/** - * Anthropic message with OpenRouter extensions - */ -export class OpenRouterAnthropicMessageParam extends S.Class( - "OpenRouterAnthropicMessageParam", -)({ - role: OpenRouterAnthropicMessageParamRole, - content: S.Union([ - S.String, - S.Array( - S.Union([ - S.Struct({ - type: S.Literal("text"), - text: S.String, - citations: S.optional(S.NullOr(S.Array( - S.Union([ - S.Struct({ - type: S.Literal("char_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_char_index: S.Number, - end_char_index: S.Number, - }), - S.Struct({ - type: S.Literal("page_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_page_number: S.Number, - end_page_number: S.Number, - }), - S.Struct({ - type: S.Literal("content_block_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_block_index: S.Number, - end_block_index: S.Number, - }), - S.Struct({ - type: S.Literal("web_search_result_location"), - cited_text: S.String, - encrypted_index: S.String, - title: S.NullOr(S.String), - url: S.String, - }), - S.Struct({ - type: S.Literal("search_result_location"), - cited_text: S.String, - search_result_index: S.Number, - source: S.String, - title: S.NullOr(S.String), - start_block_index: S.Number, - end_block_index: S.Number, - }), - ]), - ))), - cache_control: S.optional(S.NullOr(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - }))), - }), - S.Struct({ - type: S.Literal("image"), - source: S.Union([ - S.Struct({ - type: S.Literal("base64"), - media_type: S.Literals(["image/jpeg", "image/png", "image/gif", "image/webp"]), - data: S.String, - }), - S.Struct({ - type: S.Literal("url"), - url: S.String, - }), - ]), - cache_control: S.optional(S.NullOr(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - }))), - }), - S.Struct({ - type: S.Literal("document"), - source: S.Union([ - S.Struct({ - type: S.Literal("base64"), - media_type: S.Literal("application/pdf"), - data: S.String, - }), - S.Struct({ - type: S.Literal("text"), - media_type: S.Literal("text/plain"), - data: S.String, - }), - S.Struct({ - type: S.Literal("content"), - content: S.Union([ - S.String, - S.Array( - S.Union([ - S.Struct({ - type: S.Literal("text"), - text: S.String, - citations: S.optional(S.NullOr(S.Array( - S.Union([ - S.Struct({ - type: S.Literal("char_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_char_index: S.Number, - end_char_index: S.Number, - }), - S.Struct({ - type: S.Literal("page_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_page_number: S.Number, - end_page_number: S.Number, - }), - S.Struct({ - type: S.Literal("content_block_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_block_index: S.Number, - end_block_index: S.Number, - }), - S.Struct({ - type: S.Literal("web_search_result_location"), - cited_text: S.String, - encrypted_index: S.String, - title: S.NullOr(S.String), - url: S.String, - }), - S.Struct({ - type: S.Literal("search_result_location"), - cited_text: S.String, - search_result_index: S.Number, - source: S.String, - title: S.NullOr(S.String), - start_block_index: S.Number, - end_block_index: S.Number, - }), - ]), - ))), - cache_control: S.optional(S.NullOr(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - }))), - }), - S.Struct({ - type: S.Literal("image"), - source: S.Union([ - S.Struct({ - type: S.Literal("base64"), - media_type: S.Literals([ - "image/jpeg", - "image/png", - "image/gif", - "image/webp", - ]), - data: S.String, - }), - S.Struct({ - type: S.Literal("url"), - url: S.String, - }), - ]), - cache_control: S.optional(S.NullOr(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - }))), - }), - ]), - ), - ]), - }), - S.Struct({ - type: S.Literal("url"), - url: S.String, - }), - ]), - citations: S.optional(S.NullOr(S.Struct({ - enabled: S.optional(S.NullOr(S.Boolean)), - }))), - context: S.optional(S.NullOr(S.String)), - title: S.optional(S.NullOr(S.String)), - cache_control: S.optional(S.NullOr(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - }))), - }), - S.Struct({ - type: S.Literal("tool_use"), - id: S.String, - name: S.String, - cache_control: S.optional(S.NullOr(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - }))), - }), - S.Struct({ - type: S.Literal("tool_result"), - tool_use_id: S.String, - content: S.optional(S.NullOr(S.Union([ - S.String, - S.Array( - S.Union([ - S.Struct({ - type: S.Literal("text"), - text: S.String, - citations: S.optional(S.Array( - S.Union([ - S.Struct({ - type: S.Literal("char_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_char_index: S.Number, - end_char_index: S.Number, - }), - S.Struct({ - type: S.Literal("page_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_page_number: S.Number, - end_page_number: S.Number, - }), - S.Struct({ - type: S.Literal("content_block_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_block_index: S.Number, - end_block_index: S.Number, - }), - S.Struct({ - type: S.Literal("web_search_result_location"), - cited_text: S.String, - encrypted_index: S.String, - title: S.NullOr(S.String), - url: S.String, - }), - S.Struct({ - type: S.Literal("search_result_location"), - cited_text: S.String, - search_result_index: S.Number, - source: S.String, - title: S.NullOr(S.String), - start_block_index: S.Number, - end_block_index: S.Number, - }), - ]), - )), - cache_control: S.optional(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - })), - }), - S.Struct({ - type: S.Literal("image"), - source: S.Union([ - S.Struct({ - type: S.Literal("base64"), - media_type: S.Literals([ - "image/jpeg", - "image/png", - "image/gif", - "image/webp", - ]), - data: S.String, - }), - S.Struct({ - type: S.Literal("url"), - url: S.String, - }), - ]), - cache_control: S.optional(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - })), - }), - ]), - ), - ]))), - is_error: S.optional(S.NullOr(S.Boolean)), - cache_control: S.optional(S.NullOr(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - }))), - }), - S.Struct({ - type: S.Literal("thinking"), - thinking: S.String, - signature: S.String, - }), - S.Struct({ - type: S.Literal("redacted_thinking"), - data: S.String, - }), - S.Struct({ - type: S.Literal("server_tool_use"), - id: S.String, - name: S.Literal("web_search"), - cache_control: S.optional(S.NullOr(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - }))), - }), - S.Struct({ - type: S.Literal("web_search_tool_result"), - tool_use_id: S.String, - content: S.Union([ - S.Array( - S.Struct({ - type: S.Literal("web_search_result"), - encrypted_content: S.String, - title: S.String, - url: S.String, - page_age: S.optional(S.NullOr(S.String)), - }), - ), - S.Struct({ - type: S.Literal("web_search_tool_result_error"), - error_code: S.Literals([ - "invalid_tool_input", - "unavailable", - "max_uses_exceeded", - "too_many_requests", - "query_too_long", - ]), - }), - ]), - cache_control: S.optional(S.NullOr(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - }))), - }), - S.Struct({ - type: S.Literal("search_result"), - source: S.String, - title: S.String, - content: S.Array( - S.Struct({ - type: S.Literal("text"), - text: S.String, - citations: S.optional(S.NullOr(S.Array( - S.Union([ - S.Struct({ - type: S.Literal("char_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_char_index: S.Number, - end_char_index: S.Number, - }), - S.Struct({ - type: S.Literal("page_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_page_number: S.Number, - end_page_number: S.Number, - }), - S.Struct({ - type: S.Literal("content_block_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_block_index: S.Number, - end_block_index: S.Number, - }), - S.Struct({ - type: S.Literal("web_search_result_location"), - cited_text: S.String, - encrypted_index: S.String, - title: S.NullOr(S.String), - url: S.String, - }), - S.Struct({ - type: S.Literal("search_result_location"), - cited_text: S.String, - search_result_index: S.Number, - source: S.String, - title: S.NullOr(S.String), - start_block_index: S.Number, - end_block_index: S.Number, - }), - ]), - ))), - cache_control: S.optional(S.NullOr(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - }))), - }), - ), - citations: S.optional(S.NullOr(S.Struct({ - enabled: S.optional(S.NullOr(S.Boolean)), - }))), - cache_control: S.optional(S.NullOr(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - }))), - }), - ]), - ), - ]), -}) {} - -export const AnthropicMessagesRequestToolChoiceEnumType = S.Literal("tool") - -export const AnthropicMessagesRequestThinkingEnumType = S.Literal("disabled") - -export const AnthropicMessagesRequestServiceTier = S.Literals(["auto", "standard_only"]) - -/** - * The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. - */ -export const AnthropicMessagesRequestProviderSort = S.Literals(["price", "throughput", "latency"]) - -/** - * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). - */ -export const AnthropicMessagesRequestRoute = S.Literals(["fallback", "sort"]) - -/** - * Request schema for Anthropic Messages API endpoint - */ -export class AnthropicMessagesRequest extends S.Class("AnthropicMessagesRequest")({ - model: S.String, - max_tokens: S.Number, - messages: S.Array(OpenRouterAnthropicMessageParam), - system: S.optional(S.NullOr(S.Union([ - S.String, - S.Array( - S.Struct({ - type: S.Literal("text"), - text: S.String, - citations: S.optional(S.Array( - S.Union([ - S.Struct({ - type: S.Literal("char_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_char_index: S.Number, - end_char_index: S.Number, - }), - S.Struct({ - type: S.Literal("page_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_page_number: S.Number, - end_page_number: S.Number, - }), - S.Struct({ - type: S.Literal("content_block_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_block_index: S.Number, - end_block_index: S.Number, - }), - S.Struct({ - type: S.Literal("web_search_result_location"), - cited_text: S.String, - encrypted_index: S.String, - title: S.NullOr(S.String), - url: S.String, - }), - S.Struct({ - type: S.Literal("search_result_location"), - cited_text: S.String, - search_result_index: S.Number, - source: S.String, - title: S.NullOr(S.String), - start_block_index: S.Number, - end_block_index: S.Number, - }), - ]), - )), - cache_control: S.optional(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - })), - }), - ), - ]))), - metadata: S.optional(S.NullOr(S.Struct({ - user_id: S.optional(S.NullOr(S.String)), - }))), - stop_sequences: S.optional(S.NullOr(S.Array(S.String))), - stream: S.optional(S.NullOr(S.Boolean)), - temperature: S.optional(S.NullOr(S.Number)), - top_p: S.optional(S.NullOr(S.Number)), - top_k: S.optional(S.NullOr(S.Number)), - tools: S.optional(S.NullOr(S.Array( - S.Union([ - S.Struct({ - name: S.String, - description: S.optional(S.NullOr(S.String)), - input_schema: S.Struct({ - type: S.Literal("object"), - required: S.optional(S.NullOr(S.Array(S.String))), - }), - type: S.optional(S.NullOr(S.Literal("custom"))), - cache_control: S.optional(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - })), - }), - S.Struct({ - type: S.Literal("bash_20250124"), - name: S.Literal("bash"), - cache_control: S.optional(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - })), - }), - S.Struct({ - type: S.Literal("text_editor_20250124"), - name: S.Literal("str_replace_editor"), - cache_control: S.optional(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - })), - }), - S.Struct({ - type: S.Literal("web_search_20250305"), - name: S.Literal("web_search"), - allowed_domains: S.optional(S.NullOr(S.Array(S.String))), - blocked_domains: S.optional(S.NullOr(S.Array(S.String))), - max_uses: S.optional(S.NullOr(S.Number)), - user_location: S.optional(S.Struct({ - type: S.Literal("approximate"), - city: S.optional(S.NullOr(S.String)), - country: S.optional(S.NullOr(S.String)), - region: S.optional(S.NullOr(S.String)), - timezone: S.optional(S.NullOr(S.String)), - })), - cache_control: S.optional(S.Struct({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(S.Literals(["5m", "1h"]))), - })), - }), - ]), - ))), - tool_choice: S.optional(S.NullOr(S.Union([ - S.Struct({ - type: AnthropicMessagesRequestToolChoiceEnumType, - disable_parallel_tool_use: S.optional(S.NullOr(S.Boolean)), - }), - S.Struct({ - type: AnthropicMessagesRequestToolChoiceEnumType, - disable_parallel_tool_use: S.optional(S.NullOr(S.Boolean)), - }), - S.Struct({ - type: AnthropicMessagesRequestToolChoiceEnumType, - }), - S.Struct({ - type: AnthropicMessagesRequestToolChoiceEnumType, - name: S.String, - disable_parallel_tool_use: S.optional(S.NullOr(S.Boolean)), - }), - ]))), - thinking: S.optional(S.NullOr(S.Union([ - S.Struct({ - type: AnthropicMessagesRequestThinkingEnumType, - budget_tokens: S.Number, - }), - S.Struct({ - type: AnthropicMessagesRequestThinkingEnumType, - }), - ]))), - service_tier: S.optional(S.NullOr(AnthropicMessagesRequestServiceTier)), - /** - * When multiple model providers are available, optionally indicate your routing preference. - */ - provider: S.optional(S.NullOr(S.Struct({ - /** - * Whether to allow backup providers to serve requests - * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. - * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. - */ - allow_fallbacks: S.optional(S.NullOr(S.Boolean)), - /** - * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. - */ - require_parameters: S.optional(S.NullOr(S.Boolean)), - data_collection: S.optional(S.NullOr(DataCollection)), - /** - * Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used. - */ - zdr: S.optional(S.NullOr(S.Boolean)), - /** - * Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used. - */ - enforce_distillable_text: S.optional(S.NullOr(S.Boolean)), - /** - * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. - */ - order: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), - /** - * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. - */ - only: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), - /** - * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. - */ - ignore: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), - /** - * A list of quantization levels to filter the provider by. - */ - quantizations: S.optional(S.NullOr(S.Array(Quantization))), - sort: S.optional(S.NullOr(AnthropicMessagesRequestProviderSort)), - /** - * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. - */ - max_price: S.optional(S.Struct({ - prompt: S.optional(S.NullOr(BigNumberUnion)), - completion: S.optional(S.NullOr(BigNumberUnion)), - image: S.optional(S.NullOr(BigNumberUnion)), - audio: S.optional(S.NullOr(BigNumberUnion)), - request: S.optional(S.NullOr(BigNumberUnion)), - })), - preferred_min_throughput: S.optional(S.NullOr(PreferredMinThroughput)), - preferred_max_latency: S.optional(S.NullOr(PreferredMaxLatency)), - }))), - /** - * Plugins you want to enable for this request, including their settings. - */ - plugins: S.optional(S.NullOr(S.Array( - S.Union([ - S.Struct({ - id: S.Literal("auto-router"), - /** - * Set to false to disable the auto-router plugin for this request. Defaults to true. - */ - enabled: S.optional(S.NullOr(S.Boolean)), - /** - * List of model patterns to filter which models the auto-router can route between. Supports wildcards (e.g., "anthropic/*" matches all Anthropic models). When not specified, uses the default supported models list. - */ - allowed_models: S.optional(S.NullOr(S.Array(S.String))), - }), - S.Struct({ - id: S.Literal("moderation"), - }), - S.Struct({ - id: S.Literal("web"), - /** - * Set to false to disable the web-search plugin for this request. Defaults to true. - */ - enabled: S.optional(S.NullOr(S.Boolean)), - max_results: S.optional(S.NullOr(S.Number)), - search_prompt: S.optional(S.NullOr(S.String)), - engine: S.optional(S.NullOr(WebSearchEngine)), - }), - S.Struct({ - id: S.Literal("file-parser"), - /** - * Set to false to disable the file-parser plugin for this request. Defaults to true. - */ - enabled: S.optional(S.NullOr(S.Boolean)), - pdf: S.optional(S.NullOr(PDFParserOptions)), - }), - S.Struct({ - id: S.Literal("response-healing"), - /** - * Set to false to disable the response-healing plugin for this request. Defaults to true. - */ - enabled: S.optional(S.NullOr(S.Boolean)), - }), - ]), - ))), - /** - * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). - */ - route: S.optional(S.NullOr(AnthropicMessagesRequestRoute)), - /** - * A unique identifier representing your end-user, which helps distinguish between different users of your app. This allows your app to identify specific users in case of abuse reports, preventing your entire app from being affected by the actions of individual users. Maximum of 128 characters. - */ - user: S.optional(S.NullOr(S.String.check(S.isMaxLength(128)))), - /** - * A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters. - */ - session_id: S.optional(S.NullOr(S.String.check(S.isMaxLength(128)))), - models: S.optional(S.NullOr(S.Array(S.String))), -}) {} - -export const AnthropicMessagesResponseType = S.Literal("message") - -export const AnthropicMessagesResponseRole = S.Literal("assistant") - -export const AnthropicMessagesResponseStopReason = S.Literals(["end_turn", - "max_tokens", - "stop_sequence", - "tool_use", - "pause_turn", - "refusal", - "model_context_window_exceeded",]) - -export const AnthropicMessagesResponseUsageServiceTier = S.Literals(["standard", "priority", "batch"]) - -export class AnthropicMessagesResponse extends S.Class( - "AnthropicMessagesResponse", -)({ - id: S.String, - type: AnthropicMessagesResponseType, - role: AnthropicMessagesResponseRole, - content: S.Array( - S.Union([ - S.Struct({ - type: S.Literal("text"), - text: S.String, - citations: S.NullOr( - S.Array( - S.Union([ - S.Struct({ - type: S.Literal("char_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_char_index: S.Number, - end_char_index: S.Number, - file_id: S.NullOr(S.String), - }), - S.Struct({ - type: S.Literal("page_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_page_number: S.Number, - end_page_number: S.Number, - file_id: S.NullOr(S.String), - }), - S.Struct({ - type: S.Literal("content_block_location"), - cited_text: S.String, - document_index: S.Number, - document_title: S.NullOr(S.String), - start_block_index: S.Number, - end_block_index: S.Number, - file_id: S.NullOr(S.String), - }), - S.Struct({ - type: S.Literal("web_search_result_location"), - cited_text: S.String, - encrypted_index: S.String, - title: S.NullOr(S.String), - url: S.String, - }), - S.Struct({ - type: S.Literal("search_result_location"), - cited_text: S.String, - search_result_index: S.Number, - source: S.String, - title: S.NullOr(S.String), - start_block_index: S.Number, - end_block_index: S.Number, - }), - ]), - ), - ), - }), - S.Struct({ - type: S.Literal("tool_use"), - id: S.String, - name: S.String, - }), - S.Struct({ - type: S.Literal("thinking"), - thinking: S.String, - signature: S.String, - }), - S.Struct({ - type: S.Literal("redacted_thinking"), - data: S.String, - }), - S.Struct({ - type: S.Literal("server_tool_use"), - id: S.String, - name: S.Literal("web_search"), - }), - S.Struct({ - type: S.Literal("web_search_tool_result"), - tool_use_id: S.String, - content: S.Union([ - S.Array( - S.Struct({ - type: S.Literal("web_search_result"), - encrypted_content: S.String, - page_age: S.NullOr(S.String), - title: S.String, - url: S.String, - }), - ), - S.Struct({ - type: S.Literal("web_search_tool_result_error"), - error_code: S.Literals([ - "invalid_tool_input", - "unavailable", - "max_uses_exceeded", - "too_many_requests", - "query_too_long", - ]), - }), - ]), - }), - ]), - ), - model: S.String, - stop_reason: S.NullOr(AnthropicMessagesResponseStopReason), - stop_sequence: S.NullOr(S.String), - usage: S.Struct({ - input_tokens: S.Number, - output_tokens: S.Number, - cache_creation_input_tokens: S.NullOr(S.Number), - cache_read_input_tokens: S.NullOr(S.Number), - cache_creation: S.NullOr( - S.Struct({ - ephemeral_5m_input_tokens: S.Number, - ephemeral_1h_input_tokens: S.Number, - }), - ), - server_tool_use: S.NullOr( - S.Struct({ - web_search_requests: S.Number, - }), - ), - service_tier: S.NullOr(AnthropicMessagesResponseUsageServiceTier), - }), -}) {} - -export const CreateMessages400Type = S.Literal("error") - -export const CreateMessages400 = S.Struct({ - type: CreateMessages400Type, - error: S.Struct({ - type: S.String, - message: S.String, - }), -}) - -export const CreateMessages401Type = S.Literal("error") - -export const CreateMessages401 = S.Struct({ - type: CreateMessages401Type, - error: S.Struct({ - type: S.String, - message: S.String, - }), -}) - -export const CreateMessages403Type = S.Literal("error") - -export const CreateMessages403 = S.Struct({ - type: CreateMessages403Type, - error: S.Struct({ - type: S.String, - message: S.String, - }), -}) - -export const CreateMessages404Type = S.Literal("error") - -export const CreateMessages404 = S.Struct({ - type: CreateMessages404Type, - error: S.Struct({ - type: S.String, - message: S.String, - }), -}) - -export const CreateMessages429Type = S.Literal("error") - -export const CreateMessages429 = S.Struct({ - type: CreateMessages429Type, - error: S.Struct({ - type: S.String, - message: S.String, - }), -}) - -export const CreateMessages500Type = S.Literal("error") - -export const CreateMessages500 = S.Struct({ - type: CreateMessages500Type, - error: S.Struct({ - type: S.String, - message: S.String, - }), -}) - -export const CreateMessages503Type = S.Literal("error") - -export const CreateMessages503 = S.Struct({ - type: CreateMessages503Type, - error: S.Struct({ - type: S.String, - message: S.String, - }), -}) - -export const CreateMessages529Type = S.Literal("error") - -export const CreateMessages529 = S.Struct({ - type: CreateMessages529Type, - error: S.Struct({ - type: S.String, - message: S.String, - }), -}) - -export const GetUserActivityParams = S.Struct({ - /** - * Filter by a single UTC date in the last 30 days (YYYY-MM-DD format). - */ - date: S.optional(S.NullOr(S.String)), -}) - -export class ActivityItem extends S.Class("ActivityItem")({ - /** - * Date of the activity (YYYY-MM-DD format) - */ - date: S.String, - /** - * Model slug (e.g., "openai/gpt-4.1") - */ - model: S.String, - /** - * Model permaslug (e.g., "openai/gpt-4.1-2025-04-14") - */ - model_permaslug: S.String, - /** - * Unique identifier for the endpoint - */ - endpoint_id: S.String, - /** - * Name of the provider serving this endpoint - */ - provider_name: S.String, - /** - * Total cost in USD (OpenRouter credits spent) - */ - usage: S.Number, - /** - * BYOK inference cost in USD (external credits spent) - */ - byok_usage_inference: S.Number, - /** - * Number of requests made - */ - requests: S.Number, - /** - * Total prompt tokens used - */ - prompt_tokens: S.Number, - /** - * Total completion tokens generated - */ - completion_tokens: S.Number, - /** - * Total reasoning tokens used - */ - reasoning_tokens: S.Number, -}) {} - -export const GetUserActivity200 = S.Struct({ - /** - * List of activity items - */ - data: S.Array(ActivityItem), -}) - -/** - * Error data for ForbiddenResponse - */ -export class ForbiddenResponseErrorData extends S.Class( - "ForbiddenResponseErrorData", -)({ - code: S.Int, - message: S.String, - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -/** - * Forbidden - Authentication successful but insufficient permissions - */ -export class ForbiddenResponse extends S.Class("ForbiddenResponse")({ - error: ForbiddenResponseErrorData, - user_id: S.optional(S.NullOr(S.String)), -}) {} - -/** - * Total credits purchased and used - */ -export const GetCredits200 = S.Struct({ - data: S.Struct({ - /** - * Total credits purchased - */ - total_credits: S.Number, - /** - * Total credits used - */ - total_usage: S.Number, - }), -}) - -export const CreateChargeRequestChainId = S.Literals([1, 137, 8453]) - -/** - * Create a Coinbase charge for crypto payment - */ -export class CreateChargeRequest extends S.Class("CreateChargeRequest")({ - amount: S.Number, - sender: S.String, - chain_id: CreateChargeRequestChainId, -}) {} - -export const CreateCoinbaseCharge200 = S.Struct({ - data: S.Struct({ - id: S.String, - created_at: S.String, - expires_at: S.String, - web3_data: S.Struct({ - transfer_intent: S.Struct({ - call_data: S.Struct({ - deadline: S.String, - fee_amount: S.String, - id: S.String, - operator: S.String, - prefix: S.String, - recipient: S.String, - recipient_amount: S.String, - recipient_currency: S.String, - refund_destination: S.String, - signature: S.String, - }), - metadata: S.Struct({ - chain_id: S.Number, - contract_address: S.String, - sender: S.String, - }), - }), - }), - }), -}) - -export const CreateEmbeddingsRequestEncodingFormat = S.Literals(["float", "base64"]) - -/** - * The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. - */ -export const ProviderPreferencesSort = S.Literals(["price", "throughput", "latency"]) - -/** - * Provider routing preferences for the request. - */ -export class ProviderPreferences extends S.Class("ProviderPreferences")({ - /** - * Whether to allow backup providers to serve requests - * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. - * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. - */ - allow_fallbacks: S.optional(S.NullOr(S.Boolean)), - /** - * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. - */ - require_parameters: S.optional(S.NullOr(S.Boolean)), - data_collection: S.optional(S.NullOr(DataCollection)), - /** - * Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used. - */ - zdr: S.optional(S.NullOr(S.Boolean)), - /** - * Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used. - */ - enforce_distillable_text: S.optional(S.NullOr(S.Boolean)), - /** - * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. - */ - order: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), - /** - * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. - */ - only: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), - /** - * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. - */ - ignore: S.optional(S.NullOr(S.Array(S.Union([ProviderName, S.String])))), - /** - * A list of quantization levels to filter the provider by. - */ - quantizations: S.optional(S.NullOr(S.Array(Quantization))), - sort: S.optional(S.NullOr(ProviderPreferencesSort)), - /** - * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. - */ - max_price: S.optional(S.NullOr(S.Struct({ - prompt: S.optional(S.NullOr(BigNumberUnion)), - completion: S.optional(S.NullOr(BigNumberUnion)), - image: S.optional(S.NullOr(BigNumberUnion)), - audio: S.optional(S.NullOr(BigNumberUnion)), - request: S.optional(S.NullOr(BigNumberUnion)), - }))), - preferred_min_throughput: S.optional(S.NullOr(PreferredMinThroughput)), - preferred_max_latency: S.optional(S.NullOr(PreferredMaxLatency)), -}) {} - -export class CreateEmbeddingsRequest extends S.Class("CreateEmbeddingsRequest")({ - input: S.Union([ - S.String, - S.Array(S.String), - S.Array(S.Number), - S.Array(S.Array(S.Number)), - S.Array( - S.Struct({ - content: S.Array( - S.Union([ - S.Struct({ - type: S.Literal("text"), - text: S.String, - }), - S.Struct({ - type: S.Literal("image_url"), - image_url: S.Struct({ - url: S.String, - }), - }), - ]), - ), - }), - ), - ]), - model: S.String, - encoding_format: S.optional(S.NullOr(CreateEmbeddingsRequestEncodingFormat)), - dimensions: S.optional(S.NullOr(S.Int.check(S.isGreaterThan(0)))), - user: S.optional(S.NullOr(S.String)), - provider: S.optional(S.NullOr(ProviderPreferences)), - input_type: S.optional(S.NullOr(S.String)), -}) {} - -export const CreateEmbeddings200Object = S.Literal("list") - -export const CreateEmbeddings200 = S.Struct({ - id: S.optional(S.NullOr(S.String)), - object: CreateEmbeddings200Object, - data: S.Array( - S.Struct({ - object: S.Literal("embedding"), - embedding: S.Union([S.Array(S.Number), S.String]), - index: S.optional(S.NullOr(S.Number)), - }), - ), - model: S.String, - usage: S.optional(S.NullOr(S.Struct({ - prompt_tokens: S.Number, - total_tokens: S.Number, - cost: S.optional(S.NullOr(S.Number)), - }))), -}) - -/** - * Pricing information for the model - */ -export class PublicPricing extends S.Class("PublicPricing")({ - prompt: BigNumberUnion, - completion: BigNumberUnion, - request: S.optional(S.NullOr(BigNumberUnion)), - image: S.optional(S.NullOr(BigNumberUnion)), - image_token: S.optional(S.NullOr(BigNumberUnion)), - image_output: S.optional(S.NullOr(BigNumberUnion)), - audio: S.optional(S.NullOr(BigNumberUnion)), - audio_output: S.optional(S.NullOr(BigNumberUnion)), - input_audio_cache: S.optional(S.NullOr(BigNumberUnion)), - web_search: S.optional(S.NullOr(BigNumberUnion)), - internal_reasoning: S.optional(S.NullOr(BigNumberUnion)), - input_cache_read: S.optional(S.NullOr(BigNumberUnion)), - input_cache_write: S.optional(S.NullOr(BigNumberUnion)), - discount: S.optional(S.NullOr(S.Number)), -}) {} - -/** - * Tokenizer type used by the model - */ -export const ModelGroup = S.Literals(["Router", - "Media", - "Other", - "GPT", - "Claude", - "Gemini", - "Grok", - "Cohere", - "Nova", - "Qwen", - "Yi", - "DeepSeek", - "Mistral", - "Llama2", - "Llama3", - "Llama4", - "PaLM", - "RWKV", - "Qwen3",]) - -/** - * Instruction format type - */ -export const ModelArchitectureInstructType = S.Literals(["none", - "airoboros", - "alpaca", - "alpaca-modif", - "chatml", - "claude", - "code-llama", - "gemma", - "llama2", - "llama3", - "mistral", - "nemotron", - "neural", - "openchat", - "phi3", - "rwkv", - "vicuna", - "zephyr", - "deepseek-r1", - "deepseek-v3.1", - "qwq", - "qwen3",]) - -export const InputModality = S.Literals(["text", "image", "file", "audio", "video"]) - -export const OutputModality = S.Literals(["text", "image", "embeddings", "audio"]) - -/** - * Model architecture information - */ -export class ModelArchitecture extends S.Class("ModelArchitecture")({ - tokenizer: S.optional(S.NullOr(ModelGroup)), - /** - * Instruction format type - */ - instruct_type: S.optional(S.NullOr(ModelArchitectureInstructType)), - /** - * Primary modality of the model - */ - modality: S.NullOr(S.String), - /** - * Supported input modalities - */ - input_modalities: S.Array(InputModality), - /** - * Supported output modalities - */ - output_modalities: S.Array(OutputModality), -}) {} - -/** - * Information about the top provider for this model - */ -export class TopProviderInfo extends S.Class("TopProviderInfo")({ - /** - * Context length from the top provider - */ - context_length: S.optional(S.NullOr(S.Number)), - /** - * Maximum completion tokens from the top provider - */ - max_completion_tokens: S.optional(S.NullOr(S.Number)), - /** - * Whether the top provider moderates content - */ - is_moderated: S.Boolean, -}) {} - -/** - * Per-request token limits - */ -export class PerRequestLimits extends S.Class("PerRequestLimits")({ - /** - * Maximum prompt tokens per request - */ - prompt_tokens: S.Number, - /** - * Maximum completion tokens per request - */ - completion_tokens: S.Number, -}) {} - -export const Parameter = S.Literals(["temperature", - "top_p", - "top_k", - "min_p", - "top_a", - "frequency_penalty", - "presence_penalty", - "repetition_penalty", - "max_tokens", - "logit_bias", - "logprobs", - "top_logprobs", - "seed", - "response_format", - "structured_outputs", - "stop", - "tools", - "tool_choice", - "parallel_tool_calls", - "include_reasoning", - "reasoning", - "reasoning_effort", - "web_search_options", - "verbosity",]) - -/** - * Default parameters for this model - */ -export class DefaultParameters extends S.Class("DefaultParameters")({ - temperature: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(2)))), - top_p: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(1)))), - frequency_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), -}) {} - -/** - * Information about an AI model available on OpenRouter - */ -export class Model extends S.Class("Model")({ - /** - * Unique identifier for the model - */ - id: S.String, - /** - * Canonical slug for the model - */ - canonical_slug: S.String, - /** - * Hugging Face model identifier, if applicable - */ - hugging_face_id: S.optional(S.NullOr(S.String)), - /** - * Display name of the model - */ - name: S.String, - /** - * Unix timestamp of when the model was created - */ - created: S.Number, - /** - * Description of the model - */ - description: S.optional(S.NullOr(S.String)), - pricing: PublicPricing, - /** - * Maximum context length in tokens - */ - context_length: S.NullOr(S.Number), - architecture: ModelArchitecture, - top_provider: TopProviderInfo, - per_request_limits: S.NullOr(PerRequestLimits), - /** - * List of supported parameters for this model - */ - supported_parameters: S.Array(Parameter), - default_parameters: S.NullOr(DefaultParameters), - /** - * The date after which the model may be removed. ISO 8601 date string (YYYY-MM-DD) or null if no expiration. - */ - expiration_date: S.optional(S.NullOr(S.String)), -}) {} - -/** - * List of available models - */ -export const ModelsListResponseData = S.Array(Model) - -/** - * List of available models - */ -export class ModelsListResponse extends S.Class("ModelsListResponse")({ - data: ModelsListResponseData, -}) {} - -export const GetGenerationParams = S.Struct({ - id: S.String.check(S.isMinLength(1)), -}) - -/** - * Type of API used for the generation - */ -export const GetGeneration200DataApiType = S.Literals(["completions", "embeddings"]) - -/** - * Generation response - */ -export const GetGeneration200 = S.Struct({ - /** - * Generation data - */ - data: S.Struct({ - /** - * Unique identifier for the generation - */ - id: S.String, - /** - * Upstream provider's identifier for this generation - */ - upstream_id: S.NullOr(S.String), - /** - * Total cost of the generation in USD - */ - total_cost: S.Number, - /** - * Discount applied due to caching - */ - cache_discount: S.NullOr(S.Number), - /** - * Cost charged by the upstream provider - */ - upstream_inference_cost: S.NullOr(S.Number), - /** - * ISO 8601 timestamp of when the generation was created - */ - created_at: S.String, - /** - * Model used for the generation - */ - model: S.String, - /** - * ID of the app that made the request - */ - app_id: S.NullOr(S.Number), - /** - * Whether the response was streamed - */ - streamed: S.NullOr(S.Boolean), - /** - * Whether the generation was cancelled - */ - cancelled: S.NullOr(S.Boolean), - /** - * Name of the provider that served the request - */ - provider_name: S.NullOr(S.String), - /** - * Total latency in milliseconds - */ - latency: S.NullOr(S.Number), - /** - * Moderation latency in milliseconds - */ - moderation_latency: S.NullOr(S.Number), - /** - * Time taken for generation in milliseconds - */ - generation_time: S.NullOr(S.Number), - /** - * Reason the generation finished - */ - finish_reason: S.NullOr(S.String), - /** - * Number of tokens in the prompt - */ - tokens_prompt: S.NullOr(S.Number), - /** - * Number of tokens in the completion - */ - tokens_completion: S.NullOr(S.Number), - /** - * Native prompt tokens as reported by provider - */ - native_tokens_prompt: S.NullOr(S.Number), - /** - * Native completion tokens as reported by provider - */ - native_tokens_completion: S.NullOr(S.Number), - /** - * Native completion image tokens as reported by provider - */ - native_tokens_completion_images: S.NullOr(S.Number), - /** - * Native reasoning tokens as reported by provider - */ - native_tokens_reasoning: S.NullOr(S.Number), - /** - * Native cached tokens as reported by provider - */ - native_tokens_cached: S.NullOr(S.Number), - /** - * Number of media items in the prompt - */ - num_media_prompt: S.NullOr(S.Number), - /** - * Number of audio inputs in the prompt - */ - num_input_audio_prompt: S.NullOr(S.Number), - /** - * Number of media items in the completion - */ - num_media_completion: S.NullOr(S.Number), - /** - * Number of search results included - */ - num_search_results: S.NullOr(S.Number), - /** - * Origin URL of the request - */ - origin: S.String, - /** - * Usage amount in USD - */ - usage: S.Number, - /** - * Whether this used bring-your-own-key - */ - is_byok: S.Boolean, - /** - * Native finish reason as reported by provider - */ - native_finish_reason: S.NullOr(S.String), - /** - * External user identifier - */ - external_user: S.NullOr(S.String), - /** - * Type of API used for the generation - */ - api_type: S.NullOr(GetGeneration200DataApiType), - /** - * Router used for the request (e.g., openrouter/auto) - */ - router: S.NullOr(S.String), - }), -}) - -/** - * Model count data - */ -export class ModelsCountResponse extends S.Class("ModelsCountResponse")({ - /** - * Model count data - */ - data: S.Struct({ - /** - * Total number of available models - */ - count: S.Number, - }), -}) {} - -/** - * Filter models by use case category - */ -export const GetModelsParamsCategory = S.Literals(["programming", - "roleplay", - "marketing", - "marketing/seo", - "technology", - "science", - "translation", - "legal", - "finance", - "health", - "trivia", - "academia",]) - -export const GetModelsParams = S.Struct({ - /** - * Filter models by use case category - */ - category: S.optional(S.NullOr(GetModelsParamsCategory)), - supported_parameters: S.optional(S.NullOr(S.String)), -}) - -/** - * Instruction format type - */ -export const ListEndpointsResponseArchitectureEnumInstructType = S.Literals(["none", - "airoboros", - "alpaca", - "alpaca-modif", - "chatml", - "claude", - "code-llama", - "gemma", - "llama2", - "llama3", - "mistral", - "nemotron", - "neural", - "openchat", - "phi3", - "rwkv", - "vicuna", - "zephyr", - "deepseek-r1", - "deepseek-v3.1", - "qwq", - "qwen3",]) - -/** - * Model architecture information - */ -export const ListEndpointsResponseArchitecture = S.Struct({ - tokenizer: ModelGroup, - /** - * Instruction format type - */ - instruct_type: S.NullOr( - S.Literals([ - "none", - "airoboros", - "alpaca", - "alpaca-modif", - "chatml", - "claude", - "code-llama", - "gemma", - "llama2", - "llama3", - "mistral", - "nemotron", - "neural", - "openchat", - "phi3", - "rwkv", - "vicuna", - "zephyr", - "deepseek-r1", - "deepseek-v3.1", - "qwq", - "qwen3", - ]), - ), - /** - * Primary modality of the model - */ - modality: S.NullOr(S.String), - /** - * Supported input modalities - */ - input_modalities: S.Array(InputModality), - /** - * Supported output modalities - */ - output_modalities: S.Array(OutputModality), -}) - -export const PublicEndpointQuantizationEnum = S.Literals(["int4", - "int8", - "fp4", - "fp6", - "fp8", - "fp16", - "bf16", - "fp32", - "unknown",]) - -export const PublicEndpointQuantization = PublicEndpointQuantizationEnum - -export const EndpointStatus = S.Literals([0, -1, -2, -3, -5, -10]) - -/** - * Latency percentiles in milliseconds over the last 30 minutes. Latency measures time to first token. Only visible when authenticated with an API key or cookie; returns null for unauthenticated requests. - */ -export class PercentileStats extends S.Class("PercentileStats")({ - /** - * Median (50th percentile) - */ - p50: S.Number, - /** - * 75th percentile - */ - p75: S.Number, - /** - * 90th percentile - */ - p90: S.Number, - /** - * 99th percentile - */ - p99: S.Number, -}) {} - -/** - * Throughput percentiles in tokens per second over the last 30 minutes. Throughput measures output token generation speed. Only visible when authenticated with an API key or cookie; returns null for unauthenticated requests. - */ -export const PublicEndpointThroughputLast30M = S.Struct({ - /** - * Median (50th percentile) - */ - p50: S.Number, - /** - * 75th percentile - */ - p75: S.Number, - /** - * 90th percentile - */ - p90: S.Number, - /** - * 99th percentile - */ - p99: S.Number, -}) - -/** - * Information about a specific model endpoint - */ -export class PublicEndpoint extends S.Class("PublicEndpoint")({ - name: S.String, - /** - * The unique identifier for the model (permaslug) - */ - model_id: S.String, - model_name: S.String, - context_length: S.Number, - pricing: S.Struct({ - prompt: BigNumberUnion, - completion: BigNumberUnion, - request: S.optional(S.NullOr(BigNumberUnion)), - image: S.optional(S.NullOr(BigNumberUnion)), - image_token: S.optional(S.NullOr(BigNumberUnion)), - image_output: S.optional(S.NullOr(BigNumberUnion)), - audio: S.optional(S.NullOr(BigNumberUnion)), - audio_output: S.optional(S.NullOr(BigNumberUnion)), - input_audio_cache: S.optional(S.NullOr(BigNumberUnion)), - web_search: S.optional(S.NullOr(BigNumberUnion)), - internal_reasoning: S.optional(S.NullOr(BigNumberUnion)), - input_cache_read: S.optional(S.NullOr(BigNumberUnion)), - input_cache_write: S.optional(S.NullOr(BigNumberUnion)), - discount: S.optional(S.NullOr(S.Number)), - }), - provider_name: ProviderName, - tag: S.String, - quantization: PublicEndpointQuantization, - max_completion_tokens: S.NullOr(S.Number), - max_prompt_tokens: S.NullOr(S.Number), - supported_parameters: S.Array(Parameter), - status: S.optional(S.NullOr(EndpointStatus)), - uptime_last_30m: S.NullOr(S.Number), - supports_implicit_caching: S.Boolean, - latency_last_30m: S.NullOr(PercentileStats), - throughput_last_30m: PublicEndpointThroughputLast30M, -}) {} - -/** - * List of available endpoints for a model - */ -export class ListEndpointsResponse extends S.Class("ListEndpointsResponse")({ - /** - * Unique identifier for the model - */ - id: S.String, - /** - * Display name of the model - */ - name: S.String, - /** - * Unix timestamp of when the model was created - */ - created: S.Number, - /** - * Description of the model - */ - description: S.String, - architecture: ListEndpointsResponseArchitecture, - /** - * List of available endpoints for this model - */ - endpoints: S.Array(PublicEndpoint), -}) {} - -export const ListEndpoints200 = S.Struct({ - data: ListEndpointsResponse, -}) - -export const ListEndpointsZdr200 = S.Struct({ - data: S.Array(PublicEndpoint), -}) - -export const ListProviders200 = S.Struct({ - data: S.Array( - S.Struct({ - /** - * Display name of the provider - */ - name: S.String, - /** - * URL-friendly identifier for the provider - */ - slug: S.String, - /** - * URL to the provider's privacy policy - */ - privacy_policy_url: S.NullOr(S.String), - /** - * URL to the provider's terms of service - */ - terms_of_service_url: S.optional(S.NullOr(S.String)), - /** - * URL to the provider's status page - */ - status_page_url: S.optional(S.NullOr(S.String)), - }), - ), -}) - -export const ListParams = S.Struct({ - /** - * Whether to include disabled API keys in the response - */ - include_disabled: S.optional(S.NullOr(S.String)), - /** - * Number of API keys to skip for pagination - */ - offset: S.optional(S.NullOr(S.String)), -}) - -export const List200 = S.Struct({ - /** - * List of API keys - */ - data: S.Array( - S.Struct({ - /** - * Unique hash identifier for the API key - */ - hash: S.String, - /** - * Name of the API key - */ - name: S.String, - /** - * Human-readable label for the API key - */ - label: S.String, - /** - * Whether the API key is disabled - */ - disabled: S.Boolean, - /** - * Spending limit for the API key in USD - */ - limit: S.NullOr(S.Number), - /** - * Remaining spending limit in USD - */ - limit_remaining: S.NullOr(S.Number), - /** - * Type of limit reset for the API key - */ - limit_reset: S.NullOr(S.String), - /** - * Whether to include external BYOK usage in the credit limit - */ - include_byok_in_limit: S.Boolean, - /** - * Total OpenRouter credit usage (in USD) for the API key - */ - usage: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC day - */ - usage_daily: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) - */ - usage_weekly: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC month - */ - usage_monthly: S.Number, - /** - * Total external BYOK usage (in USD) for the API key - */ - byok_usage: S.Number, - /** - * External BYOK usage (in USD) for the current UTC day - */ - byok_usage_daily: S.Number, - /** - * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) - */ - byok_usage_weekly: S.Number, - /** - * External BYOK usage (in USD) for current UTC month - */ - byok_usage_monthly: S.Number, - /** - * ISO 8601 timestamp of when the API key was created - */ - created_at: S.String, - /** - * ISO 8601 timestamp of when the API key was last updated - */ - updated_at: S.NullOr(S.String), - /** - * ISO 8601 UTC timestamp when the API key expires, or null if no expiration - */ - expires_at: S.optional(S.NullOr(S.String)), - }), - ), -}) - -/** - * Type of limit reset for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. - */ -export const CreateKeysRequestLimitReset = S.Literals(["daily", "weekly", "monthly"]) - -export class CreateKeysRequest extends S.Class("CreateKeysRequest")({ - /** - * Name for the new API key - */ - name: S.String.check(S.isMinLength(1)), - /** - * Optional spending limit for the API key in USD - */ - limit: S.optional(S.NullOr(S.Number)), - /** - * Type of limit reset for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. - */ - limit_reset: S.optional(S.NullOr(CreateKeysRequestLimitReset)), - /** - * Whether to include BYOK usage in the limit - */ - include_byok_in_limit: S.optional(S.NullOr(S.Boolean)), - /** - * Optional ISO 8601 UTC timestamp when the API key should expire. Must be UTC, other timezones will be rejected - */ - expires_at: S.optional(S.NullOr(S.String)), -}) {} - -export const CreateKeys201 = S.Struct({ - /** - * The created API key information - */ - data: S.Struct({ - /** - * Unique hash identifier for the API key - */ - hash: S.String, - /** - * Name of the API key - */ - name: S.String, - /** - * Human-readable label for the API key - */ - label: S.String, - /** - * Whether the API key is disabled - */ - disabled: S.Boolean, - /** - * Spending limit for the API key in USD - */ - limit: S.NullOr(S.Number), - /** - * Remaining spending limit in USD - */ - limit_remaining: S.NullOr(S.Number), - /** - * Type of limit reset for the API key - */ - limit_reset: S.NullOr(S.String), - /** - * Whether to include external BYOK usage in the credit limit - */ - include_byok_in_limit: S.Boolean, - /** - * Total OpenRouter credit usage (in USD) for the API key - */ - usage: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC day - */ - usage_daily: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) - */ - usage_weekly: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC month - */ - usage_monthly: S.Number, - /** - * Total external BYOK usage (in USD) for the API key - */ - byok_usage: S.Number, - /** - * External BYOK usage (in USD) for the current UTC day - */ - byok_usage_daily: S.Number, - /** - * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) - */ - byok_usage_weekly: S.Number, - /** - * External BYOK usage (in USD) for current UTC month - */ - byok_usage_monthly: S.Number, - /** - * ISO 8601 timestamp of when the API key was created - */ - created_at: S.String, - /** - * ISO 8601 timestamp of when the API key was last updated - */ - updated_at: S.NullOr(S.String), - /** - * ISO 8601 UTC timestamp when the API key expires, or null if no expiration - */ - expires_at: S.optional(S.NullOr(S.String)), - }), - /** - * The actual API key string (only shown once) - */ - key: S.String, -}) - -export const GetKey200 = S.Struct({ - /** - * The API key information - */ - data: S.Struct({ - /** - * Unique hash identifier for the API key - */ - hash: S.String, - /** - * Name of the API key - */ - name: S.String, - /** - * Human-readable label for the API key - */ - label: S.String, - /** - * Whether the API key is disabled - */ - disabled: S.Boolean, - /** - * Spending limit for the API key in USD - */ - limit: S.NullOr(S.Number), - /** - * Remaining spending limit in USD - */ - limit_remaining: S.NullOr(S.Number), - /** - * Type of limit reset for the API key - */ - limit_reset: S.NullOr(S.String), - /** - * Whether to include external BYOK usage in the credit limit - */ - include_byok_in_limit: S.Boolean, - /** - * Total OpenRouter credit usage (in USD) for the API key - */ - usage: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC day - */ - usage_daily: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) - */ - usage_weekly: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC month - */ - usage_monthly: S.Number, - /** - * Total external BYOK usage (in USD) for the API key - */ - byok_usage: S.Number, - /** - * External BYOK usage (in USD) for the current UTC day - */ - byok_usage_daily: S.Number, - /** - * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) - */ - byok_usage_weekly: S.Number, - /** - * External BYOK usage (in USD) for current UTC month - */ - byok_usage_monthly: S.Number, - /** - * ISO 8601 timestamp of when the API key was created - */ - created_at: S.String, - /** - * ISO 8601 timestamp of when the API key was last updated - */ - updated_at: S.NullOr(S.String), - /** - * ISO 8601 UTC timestamp when the API key expires, or null if no expiration - */ - expires_at: S.optional(S.NullOr(S.String)), - }), -}) - -export const DeleteKeys200 = S.Struct({ - /** - * Confirmation that the API key was deleted - */ - deleted: S.Literal(true), -}) - -/** - * New limit reset type for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. - */ -export const UpdateKeysRequestLimitReset = S.Literals(["daily", "weekly", "monthly"]) - -export class UpdateKeysRequest extends S.Class("UpdateKeysRequest")({ - /** - * New name for the API key - */ - name: S.optional(S.NullOr(S.String)), - /** - * Whether to disable the API key - */ - disabled: S.optional(S.NullOr(S.Boolean)), - /** - * New spending limit for the API key in USD - */ - limit: S.optional(S.NullOr(S.Number)), - /** - * New limit reset type for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. - */ - limit_reset: S.optional(S.NullOr(UpdateKeysRequestLimitReset)), - /** - * Whether to include BYOK usage in the limit - */ - include_byok_in_limit: S.optional(S.NullOr(S.Boolean)), -}) {} - -export const UpdateKeys200 = S.Struct({ - /** - * The updated API key information - */ - data: S.Struct({ - /** - * Unique hash identifier for the API key - */ - hash: S.String, - /** - * Name of the API key - */ - name: S.String, - /** - * Human-readable label for the API key - */ - label: S.String, - /** - * Whether the API key is disabled - */ - disabled: S.Boolean, - /** - * Spending limit for the API key in USD - */ - limit: S.NullOr(S.Number), - /** - * Remaining spending limit in USD - */ - limit_remaining: S.NullOr(S.Number), - /** - * Type of limit reset for the API key - */ - limit_reset: S.NullOr(S.String), - /** - * Whether to include external BYOK usage in the credit limit - */ - include_byok_in_limit: S.Boolean, - /** - * Total OpenRouter credit usage (in USD) for the API key - */ - usage: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC day - */ - usage_daily: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) - */ - usage_weekly: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC month - */ - usage_monthly: S.Number, - /** - * Total external BYOK usage (in USD) for the API key - */ - byok_usage: S.Number, - /** - * External BYOK usage (in USD) for the current UTC day - */ - byok_usage_daily: S.Number, - /** - * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) - */ - byok_usage_weekly: S.Number, - /** - * External BYOK usage (in USD) for current UTC month - */ - byok_usage_monthly: S.Number, - /** - * ISO 8601 timestamp of when the API key was created - */ - created_at: S.String, - /** - * ISO 8601 timestamp of when the API key was last updated - */ - updated_at: S.NullOr(S.String), - /** - * ISO 8601 UTC timestamp when the API key expires, or null if no expiration - */ - expires_at: S.optional(S.NullOr(S.String)), - }), -}) - -export const ListGuardrailsParams = S.Struct({ - /** - * Number of records to skip for pagination - */ - offset: S.optional(S.NullOr(S.String)), - /** - * Maximum number of records to return (max 100) - */ - limit: S.optional(S.NullOr(S.String)), -}) - -export const ListGuardrails200 = S.Struct({ - /** - * List of guardrails - */ - data: S.Array( - S.Struct({ - /** - * Unique identifier for the guardrail - */ - id: S.String, - /** - * Name of the guardrail - */ - name: S.String, - /** - * Description of the guardrail - */ - description: S.optional(S.NullOr(S.String)), - /** - * Spending limit in USD - */ - limit_usd: S.optional(S.NullOr(S.Number.check(S.isGreaterThan(0)))), - /** - * Interval at which the limit resets (daily, weekly, monthly) - */ - reset_interval: S.optional(S.NullOr(S.Literals(["daily", "weekly", "monthly"]))), - /** - * List of allowed provider IDs - */ - allowed_providers: S.optional(S.NullOr(S.Array(S.String))), - /** - * Array of model canonical_slugs (immutable identifiers) - */ - allowed_models: S.optional(S.NullOr(S.Array(S.String))), - /** - * Whether to enforce zero data retention - */ - enforce_zdr: S.optional(S.NullOr(S.Boolean)), - /** - * ISO 8601 timestamp of when the guardrail was created - */ - created_at: S.String, - /** - * ISO 8601 timestamp of when the guardrail was last updated - */ - updated_at: S.optional(S.NullOr(S.String)), - }), - ), - /** - * Total number of guardrails - */ - total_count: S.Number, -}) - -/** - * Interval at which the limit resets (daily, weekly, monthly) - */ -export const CreateGuardrailRequestResetInterval = S.Literals(["daily", "weekly", "monthly"]) - -export class CreateGuardrailRequest extends S.Class("CreateGuardrailRequest")({ - /** - * Name for the new guardrail - */ - name: S.String.check(S.isMinLength(1), S.isMaxLength(200)), - /** - * Description of the guardrail - */ - description: S.optional(S.NullOr(S.String.check(S.isMaxLength(1000)))), - /** - * Spending limit in USD - */ - limit_usd: S.optional(S.NullOr(S.Number.check(S.isGreaterThan(0)))), - /** - * Interval at which the limit resets (daily, weekly, monthly) - */ - reset_interval: S.optional(S.NullOr(CreateGuardrailRequestResetInterval)), - /** - * List of allowed provider IDs - */ - allowed_providers: S.optional(S.NullOr(S.NonEmptyArray(S.String).check(S.isMinLength(1)))), - /** - * Array of model identifiers (slug or canonical_slug accepted) - */ - allowed_models: S.optional(S.NullOr(S.NonEmptyArray(S.String).check(S.isMinLength(1)))), - /** - * Whether to enforce zero data retention - */ - enforce_zdr: S.optional(S.NullOr(S.Boolean)), -}) {} - -/** - * Interval at which the limit resets (daily, weekly, monthly) - */ -export const CreateGuardrail201DataResetInterval = S.Literals(["daily", "weekly", "monthly"]) - -export const CreateGuardrail201 = S.Struct({ - /** - * The created guardrail - */ - data: S.Struct({ - /** - * Unique identifier for the guardrail - */ - id: S.String, - /** - * Name of the guardrail - */ - name: S.String, - /** - * Description of the guardrail - */ - description: S.optional(S.NullOr(S.String)), - /** - * Spending limit in USD - */ - limit_usd: S.optional(S.NullOr(S.Number.check(S.isGreaterThan(0)))), - /** - * Interval at which the limit resets (daily, weekly, monthly) - */ - reset_interval: S.optional(S.NullOr(CreateGuardrail201DataResetInterval)), - /** - * List of allowed provider IDs - */ - allowed_providers: S.optional(S.NullOr(S.Array(S.String))), - /** - * Array of model canonical_slugs (immutable identifiers) - */ - allowed_models: S.optional(S.NullOr(S.Array(S.String))), - /** - * Whether to enforce zero data retention - */ - enforce_zdr: S.optional(S.NullOr(S.Boolean)), - /** - * ISO 8601 timestamp of when the guardrail was created - */ - created_at: S.String, - /** - * ISO 8601 timestamp of when the guardrail was last updated - */ - updated_at: S.optional(S.NullOr(S.String)), - }), -}) - -/** - * Interval at which the limit resets (daily, weekly, monthly) - */ -export const GetGuardrail200DataResetInterval = S.Literals(["daily", "weekly", "monthly"]) - -export const GetGuardrail200 = S.Struct({ - /** - * The guardrail - */ - data: S.Struct({ - /** - * Unique identifier for the guardrail - */ - id: S.String, - /** - * Name of the guardrail - */ - name: S.String, - /** - * Description of the guardrail - */ - description: S.optional(S.NullOr(S.String)), - /** - * Spending limit in USD - */ - limit_usd: S.optional(S.NullOr(S.Number.check(S.isGreaterThan(0)))), - /** - * Interval at which the limit resets (daily, weekly, monthly) - */ - reset_interval: S.optional(S.NullOr(GetGuardrail200DataResetInterval)), - /** - * List of allowed provider IDs - */ - allowed_providers: S.optional(S.NullOr(S.Array(S.String))), - /** - * Array of model canonical_slugs (immutable identifiers) - */ - allowed_models: S.optional(S.NullOr(S.Array(S.String))), - /** - * Whether to enforce zero data retention - */ - enforce_zdr: S.optional(S.NullOr(S.Boolean)), - /** - * ISO 8601 timestamp of when the guardrail was created - */ - created_at: S.String, - /** - * ISO 8601 timestamp of when the guardrail was last updated - */ - updated_at: S.optional(S.NullOr(S.String)), - }), -}) - -export const DeleteGuardrail200 = S.Struct({ - /** - * Confirmation that the guardrail was deleted - */ - deleted: S.Literal(true), -}) - -/** - * Interval at which the limit resets (daily, weekly, monthly) - */ -export const UpdateGuardrailRequestResetInterval = S.Literals(["daily", "weekly", "monthly"]) - -export class UpdateGuardrailRequest extends S.Class("UpdateGuardrailRequest")({ - /** - * New name for the guardrail - */ - name: S.optional(S.NullOr(S.String.check(S.isMinLength(1), S.isMaxLength(200)))), - /** - * New description for the guardrail - */ - description: S.optional(S.NullOr(S.String.check(S.isMaxLength(1000)))), - /** - * New spending limit in USD - */ - limit_usd: S.optional(S.NullOr(S.Number.check(S.isGreaterThan(0)))), - /** - * Interval at which the limit resets (daily, weekly, monthly) - */ - reset_interval: S.optional(S.NullOr(UpdateGuardrailRequestResetInterval)), - /** - * New list of allowed provider IDs - */ - allowed_providers: S.optional(S.NullOr(S.NonEmptyArray(S.String).check(S.isMinLength(1)))), - /** - * Array of model identifiers (slug or canonical_slug accepted) - */ - allowed_models: S.optional(S.NullOr(S.NonEmptyArray(S.String).check(S.isMinLength(1)))), - /** - * Whether to enforce zero data retention - */ - enforce_zdr: S.optional(S.NullOr(S.Boolean)), -}) {} - -/** - * Interval at which the limit resets (daily, weekly, monthly) - */ -export const UpdateGuardrail200DataResetInterval = S.Literals(["daily", "weekly", "monthly"]) - -export const UpdateGuardrail200 = S.Struct({ - /** - * The updated guardrail - */ - data: S.Struct({ - /** - * Unique identifier for the guardrail - */ - id: S.String, - /** - * Name of the guardrail - */ - name: S.String, - /** - * Description of the guardrail - */ - description: S.optional(S.NullOr(S.String)), - /** - * Spending limit in USD - */ - limit_usd: S.optional(S.NullOr(S.Number.check(S.isGreaterThan(0)))), - /** - * Interval at which the limit resets (daily, weekly, monthly) - */ - reset_interval: S.optional(S.NullOr(UpdateGuardrail200DataResetInterval)), - /** - * List of allowed provider IDs - */ - allowed_providers: S.optional(S.NullOr(S.Array(S.String))), - /** - * Array of model canonical_slugs (immutable identifiers) - */ - allowed_models: S.optional(S.NullOr(S.Array(S.String))), - /** - * Whether to enforce zero data retention - */ - enforce_zdr: S.optional(S.NullOr(S.Boolean)), - /** - * ISO 8601 timestamp of when the guardrail was created - */ - created_at: S.String, - /** - * ISO 8601 timestamp of when the guardrail was last updated - */ - updated_at: S.optional(S.NullOr(S.String)), - }), -}) - -export const ListKeyAssignmentsParams = S.Struct({ - /** - * Number of records to skip for pagination - */ - offset: S.optional(S.NullOr(S.String)), - /** - * Maximum number of records to return (max 100) - */ - limit: S.optional(S.NullOr(S.String)), -}) - -export const ListKeyAssignments200 = S.Struct({ - /** - * List of key assignments - */ - data: S.Array( - S.Struct({ - /** - * Unique identifier for the assignment - */ - id: S.String, - /** - * Hash of the assigned API key - */ - key_hash: S.String, - /** - * ID of the guardrail - */ - guardrail_id: S.String, - /** - * Name of the API key - */ - key_name: S.String, - /** - * Label of the API key - */ - key_label: S.String, - /** - * User ID of who made the assignment - */ - assigned_by: S.NullOr(S.String), - /** - * ISO 8601 timestamp of when the assignment was created - */ - created_at: S.String, - }), - ), - /** - * Total number of key assignments for this guardrail - */ - total_count: S.Number, -}) - -export const ListMemberAssignmentsParams = S.Struct({ - /** - * Number of records to skip for pagination - */ - offset: S.optional(S.NullOr(S.String)), - /** - * Maximum number of records to return (max 100) - */ - limit: S.optional(S.NullOr(S.String)), -}) - -export const ListMemberAssignments200 = S.Struct({ - /** - * List of member assignments - */ - data: S.Array( - S.Struct({ - /** - * Unique identifier for the assignment - */ - id: S.String, - /** - * Clerk user ID of the assigned member - */ - user_id: S.String, - /** - * Organization ID - */ - organization_id: S.String, - /** - * ID of the guardrail - */ - guardrail_id: S.String, - /** - * User ID of who made the assignment - */ - assigned_by: S.NullOr(S.String), - /** - * ISO 8601 timestamp of when the assignment was created - */ - created_at: S.String, - }), - ), - /** - * Total number of member assignments - */ - total_count: S.Number, -}) - -export const ListGuardrailKeyAssignmentsParams = S.Struct({ - /** - * Number of records to skip for pagination - */ - offset: S.optional(S.NullOr(S.String)), - /** - * Maximum number of records to return (max 100) - */ - limit: S.optional(S.NullOr(S.String)), -}) - -export const ListGuardrailKeyAssignments200 = S.Struct({ - /** - * List of key assignments - */ - data: S.Array( - S.Struct({ - /** - * Unique identifier for the assignment - */ - id: S.String, - /** - * Hash of the assigned API key - */ - key_hash: S.String, - /** - * ID of the guardrail - */ - guardrail_id: S.String, - /** - * Name of the API key - */ - key_name: S.String, - /** - * Label of the API key - */ - key_label: S.String, - /** - * User ID of who made the assignment - */ - assigned_by: S.NullOr(S.String), - /** - * ISO 8601 timestamp of when the assignment was created - */ - created_at: S.String, - }), - ), - /** - * Total number of key assignments for this guardrail - */ - total_count: S.Number, -}) - -export class BulkAssignKeysToGuardrailRequest extends S.Class( - "BulkAssignKeysToGuardrailRequest", -)({ - /** - * Array of API key hashes to assign to the guardrail - */ - key_hashes: S.NonEmptyArray(S.String.check(S.isMinLength(1))).check(S.isMinLength(1)), -}) {} - -export const BulkAssignKeysToGuardrail200 = S.Struct({ - /** - * Number of keys successfully assigned - */ - assigned_count: S.Number, -}) - -export const ListGuardrailMemberAssignmentsParams = S.Struct({ - /** - * Number of records to skip for pagination - */ - offset: S.optional(S.NullOr(S.String)), - /** - * Maximum number of records to return (max 100) - */ - limit: S.optional(S.NullOr(S.String)), -}) - -export const ListGuardrailMemberAssignments200 = S.Struct({ - /** - * List of member assignments - */ - data: S.Array( - S.Struct({ - /** - * Unique identifier for the assignment - */ - id: S.String, - /** - * Clerk user ID of the assigned member - */ - user_id: S.String, - /** - * Organization ID - */ - organization_id: S.String, - /** - * ID of the guardrail - */ - guardrail_id: S.String, - /** - * User ID of who made the assignment - */ - assigned_by: S.NullOr(S.String), - /** - * ISO 8601 timestamp of when the assignment was created - */ - created_at: S.String, - }), - ), - /** - * Total number of member assignments - */ - total_count: S.Number, -}) - -export class BulkAssignMembersToGuardrailRequest extends S.Class( - "BulkAssignMembersToGuardrailRequest", -)({ - /** - * Array of member user IDs to assign to the guardrail - */ - member_user_ids: S.NonEmptyArray(S.String.check(S.isMinLength(1))).check(S.isMinLength(1)), -}) {} - -export const BulkAssignMembersToGuardrail200 = S.Struct({ - /** - * Number of members successfully assigned - */ - assigned_count: S.Number, -}) - -export class BulkUnassignKeysFromGuardrailRequest extends S.Class( - "BulkUnassignKeysFromGuardrailRequest", -)({ - /** - * Array of API key hashes to unassign from the guardrail - */ - key_hashes: S.NonEmptyArray(S.String.check(S.isMinLength(1))).check(S.isMinLength(1)), -}) {} - -export const BulkUnassignKeysFromGuardrail200 = S.Struct({ - /** - * Number of keys successfully unassigned - */ - unassigned_count: S.Number, -}) - -export class BulkUnassignMembersFromGuardrailRequest extends S.Class( - "BulkUnassignMembersFromGuardrailRequest", -)({ - /** - * Array of member user IDs to unassign from the guardrail - */ - member_user_ids: S.NonEmptyArray(S.String.check(S.isMinLength(1))).check(S.isMinLength(1)), -}) {} - -export const BulkUnassignMembersFromGuardrail200 = S.Struct({ - /** - * Number of members successfully unassigned - */ - unassigned_count: S.Number, -}) - -export const GetCurrentKey200 = S.Struct({ - /** - * Current API key information - */ - data: S.Struct({ - /** - * Human-readable label for the API key - */ - label: S.String, - /** - * Spending limit for the API key in USD - */ - limit: S.NullOr(S.Number), - /** - * Total OpenRouter credit usage (in USD) for the API key - */ - usage: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC day - */ - usage_daily: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) - */ - usage_weekly: S.Number, - /** - * OpenRouter credit usage (in USD) for the current UTC month - */ - usage_monthly: S.Number, - /** - * Total external BYOK usage (in USD) for the API key - */ - byok_usage: S.Number, - /** - * External BYOK usage (in USD) for the current UTC day - */ - byok_usage_daily: S.Number, - /** - * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) - */ - byok_usage_weekly: S.Number, - /** - * External BYOK usage (in USD) for current UTC month - */ - byok_usage_monthly: S.Number, - /** - * Whether this is a free tier API key - */ - is_free_tier: S.Boolean, - /** - * Whether this is a provisioning key - */ - is_provisioning_key: S.Boolean, - /** - * Remaining spending limit in USD - */ - limit_remaining: S.NullOr(S.Number), - /** - * Type of limit reset for the API key - */ - limit_reset: S.NullOr(S.String), - /** - * Whether to include external BYOK usage in the credit limit - */ - include_byok_in_limit: S.Boolean, - /** - * ISO 8601 UTC timestamp when the API key expires, or null if no expiration - */ - expires_at: S.optional(S.NullOr(S.String)), - /** - * Legacy rate limit information about a key. Will always return -1. - */ - rate_limit: S.Struct({ - /** - * Number of requests allowed per interval - */ - requests: S.Number, - /** - * Rate limit interval - */ - interval: S.String, - /** - * Note about the rate limit - */ - note: S.String, - }), - }), -}) - -/** - * The method used to generate the code challenge - */ -export const ExchangeAuthCodeForAPIKeyRequestCodeChallengeMethod = S.Literals(["S256", "plain"]) - -export class ExchangeAuthCodeForAPIKeyRequest extends S.Class( - "ExchangeAuthCodeForAPIKeyRequest", -)({ - /** - * The authorization code received from the OAuth redirect - */ - code: S.String, - /** - * The code verifier if code_challenge was used in the authorization request - */ - code_verifier: S.optional(S.NullOr(S.String)), - /** - * The method used to generate the code challenge - */ - code_challenge_method: S.optional(S.NullOr(ExchangeAuthCodeForAPIKeyRequestCodeChallengeMethod)), -}) {} - -export const ExchangeAuthCodeForAPIKey200 = S.Struct({ - /** - * The API key to use for OpenRouter requests - */ - key: S.String, - /** - * User ID associated with the API key - */ - user_id: S.NullOr(S.String), -}) - -/** - * The method used to generate the code challenge - */ -export const CreateAuthKeysCodeRequestCodeChallengeMethod = S.Literals(["S256", "plain"]) - -export class CreateAuthKeysCodeRequest extends S.Class( - "CreateAuthKeysCodeRequest", -)({ - /** - * The callback URL to redirect to after authorization. Note, only https URLs on ports 443 and 3000 are allowed. - */ - callback_url: S.String, - /** - * PKCE code challenge for enhanced security - */ - code_challenge: S.optional(S.NullOr(S.String)), - /** - * The method used to generate the code challenge - */ - code_challenge_method: S.optional(S.NullOr(CreateAuthKeysCodeRequestCodeChallengeMethod)), - /** - * Credit limit for the API key to be created - */ - limit: S.optional(S.NullOr(S.Number)), - /** - * Optional expiration time for the API key to be created - */ - expires_at: S.optional(S.NullOr(S.String)), -}) {} - -export const CreateAuthKeysCode200 = S.Struct({ - /** - * Auth code data - */ - data: S.Struct({ - /** - * The authorization code ID to use in the exchange request - */ - id: S.String, - /** - * The application ID associated with this auth code - */ - app_id: S.Number, - /** - * ISO 8601 timestamp of when the auth code was created - */ - created_at: S.String, - }), -}) - -export const ChatGenerationParamsProviderEnumDataCollectionEnum = S.Literals(["deny", "allow"]) - -export const Schema0 = S.Array( - S.Union([ - S.Literals([ - "AI21", - "AionLabs", - "Alibaba", - "Amazon Bedrock", - "Amazon Nova", - "Anthropic", - "Arcee AI", - "AtlasCloud", - "Avian", - "Azure", - "BaseTen", - "BytePlus", - "Black Forest Labs", - "Cerebras", - "Chutes", - "Cirrascale", - "Clarifai", - "Cloudflare", - "Cohere", - "Crusoe", - "DeepInfra", - "DeepSeek", - "Featherless", - "Fireworks", - "Friendli", - "GMICloud", - "Google", - "Google AI Studio", - "Groq", - "Hyperbolic", - "Inception", - "Inceptron", - "InferenceNet", - "Infermatic", - "Inflection", - "Liquid", - "Mara", - "Mancer 2", - "Minimax", - "ModelRun", - "Mistral", - "Modular", - "Moonshot AI", - "Morph", - "NCompass", - "Nebius", - "NextBit", - "Novita", - "Nvidia", - "OpenAI", - "OpenInference", - "Parasail", - "Perplexity", - "Phala", - "Relace", - "SambaNova", - "Seed", - "SiliconFlow", - "Sourceful", - "Stealth", - "StreamLake", - "Switchpoint", - "Together", - "Upstage", - "Venice", - "WandB", - "Xiaomi", - "xAI", - "Z.AI", - "FakeProvider", - ]), - S.String, - ]), -) - -export const ProviderSortUnion = S.Union([ProviderSort, ProviderSortConfig]) - -export const Schema1 = S.Union([S.Number, S.String, S.Number]) - -export const ChatGenerationParamsRouteEnum = S.Literals(["fallback", "sort"]) - -export const ChatMessageContentItemCacheControlTtl = S.Literals(["5m", "1h"]) - -export class ChatMessageContentItemCacheControl extends S.Class( - "ChatMessageContentItemCacheControl", -)({ - type: S.Literal("ephemeral"), - ttl: S.optional(S.NullOr(ChatMessageContentItemCacheControlTtl)), -}) {} - -export class ChatMessageContentItemText extends S.Class( - "ChatMessageContentItemText", -)({ - type: S.Literal("text"), - text: S.String, - cache_control: S.optional(S.NullOr(ChatMessageContentItemCacheControl)), -}) {} - -export class SystemMessage extends S.Class("SystemMessage")({ - role: S.Literal("system"), - content: S.Union([S.String, S.Array(ChatMessageContentItemText)]), - name: S.optional(S.NullOr(S.String)), -}) {} - -export const ChatMessageContentItemImageImageUrlDetail = S.Literals(["auto", "low", "high"]) - -export class ChatMessageContentItemImage extends S.Class( - "ChatMessageContentItemImage", -)({ - type: S.Literal("image_url"), - image_url: S.Struct({ - url: S.String, - detail: S.optional(S.NullOr(ChatMessageContentItemImageImageUrlDetail)), - }), -}) {} - -export class ChatMessageContentItemAudio extends S.Class( - "ChatMessageContentItemAudio", -)({ - type: S.Literal("input_audio"), - input_audio: S.Struct({ - data: S.String, - format: S.String, - }), -}) {} - -export const ChatMessageContentItemVideo = S.Record(S.String, S.Unknown) - -export const ChatMessageContentItem = S.Record(S.String, S.Unknown) - -export class UserMessage extends S.Class("UserMessage")({ - role: S.Literal("user"), - content: S.Union([S.String, S.Array(ChatMessageContentItem)]), - name: S.optional(S.NullOr(S.String)), -}) {} - -export class ChatMessageToolCall extends S.Class("ChatMessageToolCall")({ - id: S.String, - type: S.Literal("function"), - function: S.Struct({ - name: S.String, - arguments: S.String, - }), -}) {} - -export const Schema3 = S.Union([S.String, S.Null]) - -export const Schema4Enum = S.Literals(["unknown", - "openai-responses-v1", - "azure-openai-responses-v1", - "xai-responses-v1", - "anthropic-claude-v1", - "google-gemini-v1",]) - -export const Schema4 = S.Union([Schema4Enum, S.Null]) - -export const Schema5 = S.Number - -export const Schema2 = S.Record(S.String, S.Unknown) - -export class AssistantMessage extends S.Class("AssistantMessage")({ - role: S.Literal("assistant"), - content: S.optional(S.NullOr(S.Union([S.String, S.Array(ChatMessageContentItem)]))), - name: S.optional(S.NullOr(S.String)), - tool_calls: S.optional(S.NullOr(S.Array(ChatMessageToolCall))), - refusal: S.optional(S.NullOr(S.String)), - reasoning: S.optional(S.NullOr(S.String)), - reasoning_details: S.optional(S.NullOr(S.Array(ReasoningDetail))), - images: S.optional(S.NullOr(S.Array( - S.Struct({ - image_url: S.Struct({ - url: S.String, - }), - }), - ))), - annotations: S.optional(S.NullOr(S.Array(AnnotationDetail))), -}) {} - -export class ToolResponseMessage extends S.Class("ToolResponseMessage")({ - role: S.Literal("tool"), - content: S.Union([S.String, S.Array(ChatMessageContentItem)]), - tool_call_id: S.String, -}) {} - -export const Message = S.Record(S.String, S.Unknown) - -export const ModelName = S.String - -export const ChatGenerationParamsReasoningEffortEnum = S.Literals(["xhigh", - "high", - "medium", - "low", - "minimal", - "none",]) - -export class JSONSchemaConfig extends S.Class("JSONSchemaConfig")({ - name: S.String.check(S.isMaxLength(64)), - description: S.optional(S.NullOr(S.String)), - schema: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), - strict: S.optional(S.NullOr(S.Boolean)), -}) {} - -export class ResponseFormatJSONSchema extends S.Class("ResponseFormatJSONSchema")({ - type: S.Literal("json_schema"), - json_schema: JSONSchemaConfig, -}) {} - -export class ResponseFormatTextGrammar extends S.Class( - "ResponseFormatTextGrammar", -)({ - type: S.Literal("grammar"), - grammar: S.String, -}) {} - -export class ChatStreamOptions extends S.Class("ChatStreamOptions")({ - include_usage: S.optional(S.NullOr(S.Boolean)), -}) {} - -export class NamedToolChoice extends S.Class("NamedToolChoice")({ - type: S.Literal("function"), - function: S.Struct({ - name: S.String, - }), -}) {} - -export const ToolChoiceOption = S.Union([S.Literal("none"), - S.Literal("auto"), - S.Literal("required"), - NamedToolChoice,]) - -export class ToolDefinitionJson extends S.Class("ToolDefinitionJson")({ - type: S.Literal("function"), - function: S.Struct({ - name: S.String.check(S.isMaxLength(64)), - description: S.optional(S.NullOr(S.String)), - parameters: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), - strict: S.optional(S.NullOr(S.Boolean)), - }), -}) {} - -export class ChatGenerationParams extends S.Class("ChatGenerationParams")({ - /** - * When multiple model providers are available, optionally indicate your routing preference. - */ - provider: S.optional(S.NullOr(S.Struct({ - /** - * Whether to allow backup providers to serve requests - * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. - * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. - */ - allow_fallbacks: S.optional(S.NullOr(S.Boolean)), - /** - * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. - */ - require_parameters: S.optional(S.NullOr(S.Boolean)), - /** - * Data collection setting. If no available model provider meets the requirement, your request will return an error. - * - allow: (default) allow providers which store user data non-transiently and may train on it - * - * - deny: use only providers which do not collect user data. - */ - data_collection: S.optional(S.NullOr(S.Literals(["deny", "allow"]))), - zdr: S.optional(S.NullOr(S.Boolean)), - enforce_distillable_text: S.optional(S.NullOr(S.Boolean)), - /** - * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. - */ - order: S.optional(S.NullOr(Schema0)), - /** - * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. - */ - only: S.optional(S.NullOr(Schema0)), - /** - * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. - */ - ignore: S.optional(S.NullOr(Schema0)), - /** - * A list of quantization levels to filter the provider by. - */ - quantizations: S.optional(S.Array(S.Literals(["int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown"]))), - /** - * The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. - */ - sort: S.optional(S.NullOr(ProviderSortUnion)), - /** - * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. - */ - max_price: S.optional(S.Struct({ - prompt: S.optional(S.NullOr(Schema1)), - completion: S.optional(S.NullOr(Schema1)), - image: S.optional(S.NullOr(Schema1)), - audio: S.optional(S.NullOr(Schema1)), - request: S.optional(S.NullOr(Schema1)), - })), - /** - * Preferred minimum throughput (in tokens per second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints below the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. - */ - preferred_min_throughput: S.optional(S.Union([ - S.Number, - S.Struct({ - p50: S.optional(S.NullOr(S.Number)), - p75: S.optional(S.NullOr(S.Number)), - p90: S.optional(S.NullOr(S.Number)), - p99: S.optional(S.NullOr(S.Number)), - }), - ])), - /** - * Preferred maximum latency (in seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints above the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. - */ - preferred_max_latency: S.optional(S.Union([ - S.Number, - S.Struct({ - p50: S.optional(S.NullOr(S.Number)), - p75: S.optional(S.NullOr(S.Number)), - p90: S.optional(S.NullOr(S.Number)), - p99: S.optional(S.NullOr(S.Number)), - }), - ])), - }))), - /** - * Plugins you want to enable for this request, including their settings. - */ - plugins: S.optional(S.NullOr(S.Array(S.Record(S.String, S.Unknown)))), - route: S.optional(S.NullOr(ChatGenerationParamsRouteEnum)), - user: S.optional(S.NullOr(S.String)), - /** - * A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters. - */ - session_id: S.optional(S.NullOr(S.String.check(S.isMaxLength(128)))), - messages: S.NonEmptyArray(Message).check(S.isMinLength(1)), - model: S.optional(S.NullOr(ModelName)), - models: S.optional(S.NullOr(S.Array(ModelName))), - frequency_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), - logit_bias: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), - logprobs: S.optional(S.NullOr(S.Boolean)), - top_logprobs: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(20)))), - max_completion_tokens: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(1)))), - max_tokens: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(1)))), - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), - presence_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), - reasoning: S.optional(S.NullOr(S.Struct({ - effort: S.optional(S.NullOr(ChatGenerationParamsReasoningEffortEnum)), - summary: S.optional(S.NullOr(ReasoningSummaryVerbosity)), - }))), - response_format: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), - seed: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(-9007199254740991), S.isLessThanOrEqualTo(9007199254740991)))), - stop: S.optional(S.NullOr(S.Union([S.String, S.Array(S.String).check(S.isMaxLength(4))]))), - stream: S.NullOr(S.Boolean).pipe(S.optional, S.withDecodingDefault(() => false as const)), - stream_options: S.optional(S.NullOr(ChatStreamOptions)), - temperature: S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(2))).pipe(S.optional, S.withDecodingDefault(() => 1 as const)), - tool_choice: S.optional(S.NullOr(ToolChoiceOption)), - tools: S.optional(S.NullOr(S.Array(ToolDefinitionJson))), - top_p: S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(1))).pipe(S.optional, S.withDecodingDefault(() => 1 as const)), - debug: S.optional(S.NullOr(S.Struct({ - echo_upstream_body: S.optional(S.NullOr(S.Boolean)), - }))), - image_config: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), - modalities: S.optional(S.NullOr(S.Array(S.Literals(["text", "image"])))), -}) {} - -export const ChatCompletionFinishReason = S.Literals(["tool_calls", - "stop", - "length", - "content_filter", - "error",]) - -export const Schema6 = S.Union([ChatCompletionFinishReason, S.Null]) - -export class ChatMessageTokenLogprob extends S.Class("ChatMessageTokenLogprob")({ - token: S.String, - logprob: S.Number, - bytes: S.NullOr(S.Array(S.Number)), - top_logprobs: S.Array( - S.Struct({ - token: S.String, - logprob: S.Number, - bytes: S.NullOr(S.Array(S.Number)), - }), - ), -}) {} - -export class ChatMessageTokenLogprobs extends S.Class("ChatMessageTokenLogprobs")({ - content: S.optional(S.NullOr(S.Array(ChatMessageTokenLogprob))), - refusal: S.optional(S.NullOr(S.Array(ChatMessageTokenLogprob))), -}) {} - -export class ChatResponseChoice extends S.Class("ChatResponseChoice")({ - finish_reason: S.NullOr(ChatCompletionFinishReason), - index: S.Number, - message: AssistantMessage, - logprobs: S.optional(S.NullOr(ChatMessageTokenLogprobs)), -}) {} - -export class ChatGenerationTokenUsage extends S.Class("ChatGenerationTokenUsage")({ - completion_tokens: S.Number, - prompt_tokens: S.Number, - total_tokens: S.Number, - completion_tokens_details: S.optional(S.NullOr(S.Struct({ - reasoning_tokens: S.optional(S.NullOr(S.Number)), - audio_tokens: S.optional(S.NullOr(S.Number)), - accepted_prediction_tokens: S.optional(S.NullOr(S.Number)), - rejected_prediction_tokens: S.optional(S.NullOr(S.Number)), - }))), - prompt_tokens_details: S.optional(S.NullOr(S.Struct({ - cached_tokens: S.optional(S.NullOr(S.Number)), - cache_write_tokens: S.optional(S.NullOr(S.Number)), - audio_tokens: S.optional(S.NullOr(S.Number)), - video_tokens: S.optional(S.NullOr(S.Number)), - }))), - cost: S.optional(S.NullOr(S.Number)), - cost_details: S.optional(S.NullOr(S.Struct({ upstream_inference_cost: S.optional(S.NullOr(S.Number)) }))), -}) {} - -export class ChatResponse extends S.Class("ChatResponse")({ - id: S.String, - provider: S.optional(S.NullOr(S.String)), - choices: S.Array(ChatResponseChoice), - created: S.Number, - model: S.String, - object: S.Literal("chat.completion"), - system_fingerprint: S.optional(S.NullOr(S.String)), - usage: S.optional(S.NullOr(ChatGenerationTokenUsage)), -}) {} - -export class ChatError extends S.Class("ChatError")({ - error: S.Struct({ - code: S.NullOr(S.Union([S.String, S.Number])), - message: S.String, - param: S.optional(S.NullOr(S.String)), - type: S.optional(S.NullOr(S.String)), - }), -}) {} - -export class CompletionCreateParams extends S.Class("CompletionCreateParams")({ - model: S.optional(S.NullOr(ModelName)), - models: S.optional(S.NullOr(S.Array(ModelName))), - prompt: S.Union([S.String, S.Array(S.String), S.Array(S.Number), S.Array(S.Array(S.Number))]), - best_of: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(1), S.isLessThanOrEqualTo(20)))), - echo: S.optional(S.NullOr(S.Boolean)), - frequency_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), - logit_bias: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), - logprobs: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(5)))), - max_tokens: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(1), S.isLessThanOrEqualTo(9007199254740991)))), - n: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(1), S.isLessThanOrEqualTo(128)))), - presence_penalty: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(-2), S.isLessThanOrEqualTo(2)))), - seed: S.optional(S.NullOr(S.Int.check(S.isGreaterThanOrEqualTo(-9007199254740991), S.isLessThanOrEqualTo(9007199254740991)))), - stop: S.optional(S.NullOr(S.Union([S.String, S.Array(S.String)]))), - stream: S.NullOr(S.Boolean).pipe(S.optional, S.withDecodingDefault(() => false as const)), - stream_options: S.optional(S.NullOr(S.Struct({ - include_usage: S.optional(S.NullOr(S.Boolean)), - }))), - suffix: S.optional(S.NullOr(S.String)), - temperature: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(2)))), - top_p: S.optional(S.NullOr(S.Number.check(S.isGreaterThanOrEqualTo(0), S.isLessThanOrEqualTo(1)))), - user: S.optional(S.NullOr(S.String)), - metadata: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), - response_format: S.optional(S.NullOr(S.Record(S.String, S.Unknown))), -}) {} - -export class CompletionLogprobs extends S.Class("CompletionLogprobs")({ - tokens: S.Array(S.String), - token_logprobs: S.Array(S.Number), - top_logprobs: S.NullOr(S.Array(S.Record(S.String, S.Unknown))), - text_offset: S.Array(S.Number), -}) {} - -export const CompletionFinishReasonEnum = S.Literals(["stop", "length", "content_filter"]) - -export const CompletionFinishReason = S.Union([CompletionFinishReasonEnum, S.Null]) - -export class CompletionChoice extends S.Class("CompletionChoice")({ - text: S.String, - index: S.Number, - logprobs: S.NullOr(CompletionLogprobs), - finish_reason: S.NullOr(S.Literals(["stop", "length", "content_filter"])), - native_finish_reason: S.optional(S.NullOr(S.String)), - reasoning: S.optional(S.NullOr(S.String)), -}) {} - -export class CompletionUsage extends S.Class("CompletionUsage")({ - prompt_tokens: S.Number, - completion_tokens: S.Number, - total_tokens: S.Number, -}) {} - -export class CompletionResponse extends S.Class("CompletionResponse")({ - id: S.String, - object: S.Literal("text_completion"), - created: S.Number, - model: S.String, - provider: S.optional(S.NullOr(S.String)), - system_fingerprint: S.optional(S.NullOr(S.String)), - choices: S.Array(CompletionChoice), - usage: S.optional(S.NullOr(CompletionUsage)), -}) {} - -export const make = ( - httpClient: HttpClient.HttpClient, - options: { - readonly transformClient?: - | ((client: HttpClient.HttpClient) => Effect.Effect) - | undefined - } = {}, -): Client => { - const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => - Effect.flatMap( - Effect.orElseSucceed(response.json, () => "Unexpected status code"), - (description) => - Effect.fail( - new HttpClientError.StatusCodeError({ - request: response.request, - response, - description: - typeof description === "string" ? description : JSON.stringify(description), - }), - ), - ) - const withResponse: ( - f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect, - ) => (request: HttpClientRequest.HttpClientRequest) => Effect.Effect = options.transformClient - ? (f) => (request) => - Effect.flatMap( - Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), - f, - ) - : (f) => (request) => Effect.flatMap(httpClient.execute(request), f) - const decodeSuccess = - (schema: S.Top) => - (response: HttpClientResponse.HttpClientResponse) => - HttpClientResponse.schemaBodyJson(schema)(response) - const decodeError = - (tag: Tag, schema: S.Top) => - (response: HttpClientResponse.HttpClientResponse) => - Effect.flatMap(HttpClientResponse.schemaBodyJson(schema)(response), (cause: any) => - Effect.fail(ClientError(tag, cause, response)), - ) - return ({ - httpClient, - createResponses: (options: any) => - HttpClientRequest.post(`/responses`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(OpenResponsesNonStreamingResponse), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "402": decodeError("PaymentRequiredResponse", PaymentRequiredResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "408": decodeError("RequestTimeoutResponse", RequestTimeoutResponse), - "413": decodeError("PayloadTooLargeResponse", PayloadTooLargeResponse), - "422": decodeError("UnprocessableEntityResponse", UnprocessableEntityResponse), - "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - "502": decodeError("BadGatewayResponse", BadGatewayResponse), - "503": decodeError("ServiceUnavailableResponse", ServiceUnavailableResponse), - "524": decodeError("EdgeNetworkTimeoutResponse", EdgeNetworkTimeoutResponse), - "529": decodeError("ProviderOverloadedResponse", ProviderOverloadedResponse), - orElse: unexpectedStatus, - }), - ), - ), - createMessages: (options: any) => - HttpClientRequest.post(`/messages`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(AnthropicMessagesResponse), - "400": decodeError("CreateMessages400", CreateMessages400), - "401": decodeError("CreateMessages401", CreateMessages401), - "403": decodeError("CreateMessages403", CreateMessages403), - "404": decodeError("CreateMessages404", CreateMessages404), - "429": decodeError("CreateMessages429", CreateMessages429), - "500": decodeError("CreateMessages500", CreateMessages500), - "503": decodeError("CreateMessages503", CreateMessages503), - "529": decodeError("CreateMessages529", CreateMessages529), - orElse: unexpectedStatus, - }), - ), - ), - getUserActivity: (options: any) => - HttpClientRequest.get(`/activity`).pipe( - HttpClientRequest.setUrlParams({ date: options?.["date"] as any }), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetUserActivity200), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "403": decodeError("ForbiddenResponse", ForbiddenResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - getCredits: () => - HttpClientRequest.get(`/credits`).pipe( - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetCredits200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "403": decodeError("ForbiddenResponse", ForbiddenResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - createCoinbaseCharge: (options: any) => - HttpClientRequest.post(`/credits/coinbase`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CreateCoinbaseCharge200), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - createEmbeddings: (options: any) => - HttpClientRequest.post(`/embeddings`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CreateEmbeddings200), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "402": decodeError("PaymentRequiredResponse", PaymentRequiredResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - "502": decodeError("BadGatewayResponse", BadGatewayResponse), - "503": decodeError("ServiceUnavailableResponse", ServiceUnavailableResponse), - "524": decodeError("EdgeNetworkTimeoutResponse", EdgeNetworkTimeoutResponse), - "529": decodeError("ProviderOverloadedResponse", ProviderOverloadedResponse), - orElse: unexpectedStatus, - }), - ), - ), - listEmbeddingsModels: () => - HttpClientRequest.get(`/embeddings/models`).pipe( - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ModelsListResponse), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - getGeneration: (options: any) => - HttpClientRequest.get(`/generation`).pipe( - HttpClientRequest.setUrlParams({ id: options?.["id"] as any }), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetGeneration200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "402": decodeError("PaymentRequiredResponse", PaymentRequiredResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - "502": decodeError("BadGatewayResponse", BadGatewayResponse), - "524": decodeError("EdgeNetworkTimeoutResponse", EdgeNetworkTimeoutResponse), - "529": decodeError("ProviderOverloadedResponse", ProviderOverloadedResponse), - orElse: unexpectedStatus, - }), - ), - ), - listModelsCount: () => - HttpClientRequest.get(`/models/count`).pipe( - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ModelsCountResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - getModels: (options: any) => - HttpClientRequest.get(`/models`).pipe( - HttpClientRequest.setUrlParams({ - category: options?.["category"] as any, - supported_parameters: options?.["supported_parameters"] as any, - }), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ModelsListResponse), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - listModelsUser: () => - HttpClientRequest.get(`/models/user`).pipe( - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ModelsListResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - listEndpoints: (author: any, slug: any) => - HttpClientRequest.get(`/models/${author}/${slug}/endpoints`).pipe( - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListEndpoints200), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - listEndpointsZdr: () => - HttpClientRequest.get(`/endpoints/zdr`).pipe( - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListEndpointsZdr200), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - listProviders: () => - HttpClientRequest.get(`/providers`).pipe( - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListProviders200), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - list: (options: any) => - HttpClientRequest.get(`/keys`).pipe( - HttpClientRequest.setUrlParams({ - include_disabled: options?.["include_disabled"] as any, - offset: options?.["offset"] as any, - }), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(List200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - createKeys: (options: any) => - HttpClientRequest.post(`/keys`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CreateKeys201), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - getKey: (hash: any) => - HttpClientRequest.get(`/keys/${hash}`).pipe( - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetKey200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - deleteKeys: (hash: any) => - HttpClientRequest.delete(`/keys/${hash}`).pipe( - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(DeleteKeys200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - updateKeys: (hash: any, options: any) => - HttpClientRequest.patch(`/keys/${hash}`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(UpdateKeys200), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "429": decodeError("TooManyRequestsResponse", TooManyRequestsResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - listGuardrails: (options: any) => - HttpClientRequest.get(`/guardrails`).pipe( - HttpClientRequest.setUrlParams({ - offset: options?.["offset"] as any, - limit: options?.["limit"] as any, - }), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListGuardrails200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - createGuardrail: (options: any) => - HttpClientRequest.post(`/guardrails`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CreateGuardrail201), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - getGuardrail: (id: any) => - HttpClientRequest.get(`/guardrails/${id}`).pipe( - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetGuardrail200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - deleteGuardrail: (id: any) => - HttpClientRequest.delete(`/guardrails/${id}`).pipe( - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(DeleteGuardrail200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - updateGuardrail: (id: any, options: any) => - HttpClientRequest.patch(`/guardrails/${id}`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(UpdateGuardrail200), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - listKeyAssignments: (options: any) => - HttpClientRequest.get(`/guardrails/assignments/keys`).pipe( - HttpClientRequest.setUrlParams({ - offset: options?.["offset"] as any, - limit: options?.["limit"] as any, - }), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListKeyAssignments200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - listMemberAssignments: (options: any) => - HttpClientRequest.get(`/guardrails/assignments/members`).pipe( - HttpClientRequest.setUrlParams({ - offset: options?.["offset"] as any, - limit: options?.["limit"] as any, - }), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListMemberAssignments200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - listGuardrailKeyAssignments: (id: any, options: any) => - HttpClientRequest.get(`/guardrails/${id}/assignments/keys`).pipe( - HttpClientRequest.setUrlParams({ - offset: options?.["offset"] as any, - limit: options?.["limit"] as any, - }), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListGuardrailKeyAssignments200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - bulkAssignKeysToGuardrail: (id: any, options: any) => - HttpClientRequest.post(`/guardrails/${id}/assignments/keys`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(BulkAssignKeysToGuardrail200), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - listGuardrailMemberAssignments: (id: any, options: any) => - HttpClientRequest.get(`/guardrails/${id}/assignments/members`).pipe( - HttpClientRequest.setUrlParams({ - offset: options?.["offset"] as any, - limit: options?.["limit"] as any, - }), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListGuardrailMemberAssignments200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - bulkAssignMembersToGuardrail: (id: any, options: any) => - HttpClientRequest.post(`/guardrails/${id}/assignments/members`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(BulkAssignMembersToGuardrail200), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - bulkUnassignKeysFromGuardrail: (id: any, options: any) => - HttpClientRequest.post(`/guardrails/${id}/assignments/keys/remove`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(BulkUnassignKeysFromGuardrail200), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - bulkUnassignMembersFromGuardrail: (id: any, options: any) => - HttpClientRequest.post(`/guardrails/${id}/assignments/members/remove`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(BulkUnassignMembersFromGuardrail200), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - getCurrentKey: () => - HttpClientRequest.get(`/key`).pipe( - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetCurrentKey200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - exchangeAuthCodeForAPIKey: (options: any) => - HttpClientRequest.post(`/auth/keys`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ExchangeAuthCodeForAPIKey200), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "403": decodeError("ForbiddenResponse", ForbiddenResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - createAuthKeysCode: (options: any) => - HttpClientRequest.post(`/auth/keys/code`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CreateAuthKeysCode200), - "400": decodeError("BadRequestResponse", BadRequestResponse), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus, - }), - ), - ), - sendChatCompletionRequest: (options: any) => - HttpClientRequest.post(`/chat/completions`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ChatResponse), - "400": decodeError("ChatError", ChatError), - "401": decodeError("ChatError", ChatError), - "429": decodeError("ChatError", ChatError), - "500": decodeError("ChatError", ChatError), - orElse: unexpectedStatus, - }), - ), - ), - createCompletions: (options: any) => - HttpClientRequest.post(`/completions`).pipe( - HttpClientRequest.bodyJsonUnsafe(options), - withResponse( - HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CompletionResponse), - "400": decodeError("ChatError", ChatError), - "401": decodeError("ChatError", ChatError), - "429": decodeError("ChatError", ChatError), - "500": decodeError("ChatError", ChatError), - orElse: unexpectedStatus, - }), - ), - ), - }) as any as Client -} - -export interface Client { - readonly httpClient: HttpClient.HttpClient - /** - * Creates a streaming or non-streaming response using OpenResponses API format - */ - readonly createResponses: ( - options: typeof OpenResponsesRequest.Encoded, - ) => Effect.Effect< - typeof OpenResponsesNonStreamingResponse.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"PaymentRequiredResponse", typeof PaymentRequiredResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"RequestTimeoutResponse", typeof RequestTimeoutResponse.Type> - | ClientError<"PayloadTooLargeResponse", typeof PayloadTooLargeResponse.Type> - | ClientError<"UnprocessableEntityResponse", typeof UnprocessableEntityResponse.Type> - | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - | ClientError<"BadGatewayResponse", typeof BadGatewayResponse.Type> - | ClientError<"ServiceUnavailableResponse", typeof ServiceUnavailableResponse.Type> - | ClientError<"EdgeNetworkTimeoutResponse", typeof EdgeNetworkTimeoutResponse.Type> - | ClientError<"ProviderOverloadedResponse", typeof ProviderOverloadedResponse.Type> - > - /** - * Creates a message using the Anthropic Messages API format. Supports text, images, PDFs, tools, and extended thinking. - */ - readonly createMessages: ( - options: typeof AnthropicMessagesRequest.Encoded, - ) => Effect.Effect< - typeof AnthropicMessagesResponse.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"CreateMessages400", typeof CreateMessages400.Type> - | ClientError<"CreateMessages401", typeof CreateMessages401.Type> - | ClientError<"CreateMessages403", typeof CreateMessages403.Type> - | ClientError<"CreateMessages404", typeof CreateMessages404.Type> - | ClientError<"CreateMessages429", typeof CreateMessages429.Type> - | ClientError<"CreateMessages500", typeof CreateMessages500.Type> - | ClientError<"CreateMessages503", typeof CreateMessages503.Type> - | ClientError<"CreateMessages529", typeof CreateMessages529.Type> - > - /** - * Returns user activity data grouped by endpoint for the last 30 (completed) UTC days. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly getUserActivity: ( - options?: typeof GetUserActivityParams.Encoded | undefined, - ) => Effect.Effect< - typeof GetUserActivity200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"ForbiddenResponse", typeof ForbiddenResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Get total credits purchased and used for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly getCredits: () => Effect.Effect< - typeof GetCredits200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"ForbiddenResponse", typeof ForbiddenResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Create a Coinbase charge for crypto payment - */ - readonly createCoinbaseCharge: ( - options: typeof CreateChargeRequest.Encoded, - ) => Effect.Effect< - typeof CreateCoinbaseCharge200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Submits an embedding request to the embeddings router - */ - readonly createEmbeddings: ( - options: typeof CreateEmbeddingsRequest.Encoded, - ) => Effect.Effect< - typeof CreateEmbeddings200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"PaymentRequiredResponse", typeof PaymentRequiredResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - | ClientError<"BadGatewayResponse", typeof BadGatewayResponse.Type> - | ClientError<"ServiceUnavailableResponse", typeof ServiceUnavailableResponse.Type> - | ClientError<"EdgeNetworkTimeoutResponse", typeof EdgeNetworkTimeoutResponse.Type> - | ClientError<"ProviderOverloadedResponse", typeof ProviderOverloadedResponse.Type> - > - /** - * Returns a list of all available embeddings models and their properties - */ - readonly listEmbeddingsModels: () => Effect.Effect< - typeof ModelsListResponse.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Get request & usage metadata for a generation - */ - readonly getGeneration: ( - options: typeof GetGenerationParams.Encoded, - ) => Effect.Effect< - typeof GetGeneration200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"PaymentRequiredResponse", typeof PaymentRequiredResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - | ClientError<"BadGatewayResponse", typeof BadGatewayResponse.Type> - | ClientError<"EdgeNetworkTimeoutResponse", typeof EdgeNetworkTimeoutResponse.Type> - | ClientError<"ProviderOverloadedResponse", typeof ProviderOverloadedResponse.Type> - > - /** - * Get total count of available models - */ - readonly listModelsCount: () => Effect.Effect< - typeof ModelsCountResponse.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * List all models and their properties - */ - readonly getModels: ( - options?: typeof GetModelsParams.Encoded | undefined, - ) => Effect.Effect< - typeof ModelsListResponse.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * List models filtered by user provider preferences - */ - readonly listModelsUser: () => Effect.Effect< - typeof ModelsListResponse.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * List all endpoints for a model - */ - readonly listEndpoints: ( - author: string, - slug: string, - ) => Effect.Effect< - typeof ListEndpoints200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Preview the impact of ZDR on the available endpoints - */ - readonly listEndpointsZdr: () => Effect.Effect< - typeof ListEndpointsZdr200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * List all providers - */ - readonly listProviders: () => Effect.Effect< - typeof ListProviders200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * List all API keys for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly list: ( - options?: typeof ListParams.Encoded | undefined, - ) => Effect.Effect< - typeof List200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Create a new API key for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly createKeys: ( - options: typeof CreateKeysRequest.Encoded, - ) => Effect.Effect< - typeof CreateKeys201.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Get a single API key by hash. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly getKey: ( - hash: string, - ) => Effect.Effect< - typeof GetKey200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Delete an existing API key. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly deleteKeys: ( - hash: string, - ) => Effect.Effect< - typeof DeleteKeys200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Update an existing API key. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly updateKeys: ( - hash: string, - options: typeof UpdateKeysRequest.Encoded, - ) => Effect.Effect< - typeof UpdateKeys200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * List all guardrails for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly listGuardrails: ( - options?: typeof ListGuardrailsParams.Encoded | undefined, - ) => Effect.Effect< - typeof ListGuardrails200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Create a new guardrail for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly createGuardrail: ( - options: typeof CreateGuardrailRequest.Encoded, - ) => Effect.Effect< - typeof CreateGuardrail201.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Get a single guardrail by ID. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly getGuardrail: ( - id: string, - ) => Effect.Effect< - typeof GetGuardrail200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Delete an existing guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly deleteGuardrail: ( - id: string, - ) => Effect.Effect< - typeof DeleteGuardrail200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Update an existing guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly updateGuardrail: ( - id: string, - options: typeof UpdateGuardrailRequest.Encoded, - ) => Effect.Effect< - typeof UpdateGuardrail200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * List all API key guardrail assignments for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly listKeyAssignments: ( - options?: typeof ListKeyAssignmentsParams.Encoded | undefined, - ) => Effect.Effect< - typeof ListKeyAssignments200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * List all organization member guardrail assignments for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly listMemberAssignments: ( - options?: typeof ListMemberAssignmentsParams.Encoded | undefined, - ) => Effect.Effect< - typeof ListMemberAssignments200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * List all API key assignments for a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly listGuardrailKeyAssignments: ( - id: string, - options?: typeof ListGuardrailKeyAssignmentsParams.Encoded | undefined, - ) => Effect.Effect< - typeof ListGuardrailKeyAssignments200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Assign multiple API keys to a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly bulkAssignKeysToGuardrail: ( - id: string, - options: typeof BulkAssignKeysToGuardrailRequest.Encoded, - ) => Effect.Effect< - typeof BulkAssignKeysToGuardrail200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * List all organization member assignments for a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly listGuardrailMemberAssignments: ( - id: string, - options?: typeof ListGuardrailMemberAssignmentsParams.Encoded | undefined, - ) => Effect.Effect< - typeof ListGuardrailMemberAssignments200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Assign multiple organization members to a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly bulkAssignMembersToGuardrail: ( - id: string, - options: typeof BulkAssignMembersToGuardrailRequest.Encoded, - ) => Effect.Effect< - typeof BulkAssignMembersToGuardrail200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Unassign multiple API keys from a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly bulkUnassignKeysFromGuardrail: ( - id: string, - options: typeof BulkUnassignKeysFromGuardrailRequest.Encoded, - ) => Effect.Effect< - typeof BulkUnassignKeysFromGuardrail200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Unassign multiple organization members from a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. - */ - readonly bulkUnassignMembersFromGuardrail: ( - id: string, - options: typeof BulkUnassignMembersFromGuardrailRequest.Encoded, - ) => Effect.Effect< - typeof BulkUnassignMembersFromGuardrail200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Get information on the API key associated with the current authentication session - */ - readonly getCurrentKey: () => Effect.Effect< - typeof GetCurrentKey200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Exchange an authorization code from the PKCE flow for a user-controlled API key - */ - readonly exchangeAuthCodeForAPIKey: ( - options: typeof ExchangeAuthCodeForAPIKeyRequest.Encoded, - ) => Effect.Effect< - typeof ExchangeAuthCodeForAPIKey200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"ForbiddenResponse", typeof ForbiddenResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Create an authorization code for the PKCE flow to generate a user-controlled API key - */ - readonly createAuthKeysCode: ( - options: typeof CreateAuthKeysCodeRequest.Encoded, - ) => Effect.Effect< - typeof CreateAuthKeysCode200.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > - /** - * Sends a request for a model response for the given chat conversation. Supports both streaming and non-streaming modes. - */ - readonly sendChatCompletionRequest: ( - options: typeof ChatGenerationParams.Encoded, - ) => Effect.Effect< - typeof ChatResponse.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"ChatError", typeof ChatError.Type> - | ClientError<"ChatError", typeof ChatError.Type> - | ClientError<"ChatError", typeof ChatError.Type> - | ClientError<"ChatError", typeof ChatError.Type> - > - /** - * Creates a completion for the provided prompt and parameters. Supports both streaming and non-streaming modes. - */ - readonly createCompletions: ( - options: typeof CompletionCreateParams.Encoded, - ) => Effect.Effect< - typeof CompletionResponse.Type, - | HttpClientError.HttpClientError - | S.SchemaError - | ClientError<"ChatError", typeof ChatError.Type> - | ClientError<"ChatError", typeof ChatError.Type> - | ClientError<"ChatError", typeof ChatError.Type> - | ClientError<"ChatError", typeof ChatError.Type> - > -} - -export interface ClientError { - readonly _tag: Tag - readonly request: HttpClientRequest.HttpClientRequest - readonly response: HttpClientResponse.HttpClientResponse - readonly cause: E -} - -class ClientErrorImpl extends Data.Error<{ - _tag: string - cause: any - request: HttpClientRequest.HttpClientRequest - response: HttpClientResponse.HttpClientResponse -}> {} - -export const ClientError = ( - tag: Tag, - cause: E, - response: HttpClientResponse.HttpClientResponse, -): ClientError => - new ClientErrorImpl({ - _tag: tag, - cause, - response, - request: response.request, - }) as any diff --git a/libs/ai-openrouter/src/OpenRouterClient.ts b/libs/ai-openrouter/src/OpenRouterClient.ts deleted file mode 100644 index 39ddd6771..000000000 --- a/libs/ai-openrouter/src/OpenRouterClient.ts +++ /dev/null @@ -1,373 +0,0 @@ -/** - * @since 1.0.0 - */ -import * as AiError from "@effect/ai/AiError" -import * as Sse from "@effect/experimental/Sse" -import * as HttpBody from "@effect/platform/HttpBody" -import * as HttpClient from "@effect/platform/HttpClient" -import * as HttpClientRequest from "@effect/platform/HttpClientRequest" -import * as Config from "effect/Config" -import type { ConfigError } from "effect/ConfigError" -import * as Effect from "effect/Effect" -import { identity } from "effect/Function" -import * as Layer from "effect/Layer" -import type * as Redacted from "effect/Redacted" -import * as Schema from "effect/Schema" -import * as ServiceMap from "effect/ServiceMap" -import * as Stream from "effect/Stream" -import * as Generated from "./Generated.js" -import { OpenRouterConfig } from "./OpenRouterConfig.js" - -/** - * @since 1.0.0 - * @category Context - */ -export class OpenRouterClient extends ServiceMap.Service()("@effect/ai-openrouter/OpenRouterClient") {} - -/** - * @since 1.0.0 - * @category Models - */ -export interface Service { - /** - * The underlying HTTP client capable of communicating with the OpenRouter API. - * - * This client is pre-configured with authentication, base URL, and standard - * headers required for OpenRouter API communication. It provides direct access - * to the generated OpenRouter API client for operations not covered by the - * higher-level methods. - * - * Use this when you need to: - * - Access provider-specific API endpoints not available through the AI SDK - * - Implement custom request/response handling - * - Use OpenRouter API features not yet supported by the Effect AI abstractions - * - Perform batch operations or non-streaming requests - * - * The client automatically handles authentication and follows OpenRouter's - * API conventions for request formatting and error handling. - */ - readonly client: Generated.Client - - readonly createChatCompletion: ( - options: typeof Generated.ChatGenerationParams.Encoded, - ) => Effect.Effect - - readonly createChatCompletionStream: ( - options: Omit, - ) => Stream.Stream -} - -/** - * @since 1.0.0 - * @category Constructors - */ -export const make: (options: { - readonly apiKey?: Redacted.Redacted | undefined - readonly apiUrl?: string | undefined - /** - * Optional URL of your site for rankings on `openrouter.ai`. - */ - readonly referrer?: string | undefined - /** - * Optional title of your site for rankings on `openrouter.ai`. - */ - readonly title?: string | undefined - /** - * A function to transform the underlying HTTP client before it's used to send - * API requests. - * - * This transformation function receives the configured HTTP client and returns - * a modified version. It's applied after all standard client configuration - * (authentication, base URL, headers) but before any requests are made. - * - * Use this for: - * - Adding custom middleware (logging, metrics, caching) - * - Modifying request/response processing behavior - * - Adding custom retry logic or error handling - * - Integrating with monitoring or debugging tools - * - Applying organization-specific HTTP client policies - * - * The transformation is applied once during client initialization and affects - * all subsequent API requests made through this client instance. - * - * Leave absent or set to `undefined` if no custom HTTP client behavior is - * needed. - */ - readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined -}) => Effect.Effect = Effect.fnUntraced(function* (options) { - const httpClient = (yield* HttpClient.HttpClient).pipe( - HttpClient.mapRequest((request) => - request.pipe( - HttpClientRequest.prependUrl(options.apiUrl ?? "https://openrouter.ai/api/v1"), - options.apiKey ? HttpClientRequest.bearerToken(options.apiKey) : identity, - options.referrer ? HttpClientRequest.setHeader("HTTP-Referrer", options.referrer) : identity, - options.title ? HttpClientRequest.setHeader("X-Title", options.title) : identity, - HttpClientRequest.acceptJson, - ), - ), - options.transformClient ?? identity, - ) - - const httpClientOk = HttpClient.filterStatusOk(httpClient) - - const client = Generated.make(httpClient, { - transformClient: (client) => - OpenRouterConfig.getOrUndefined.pipe( - Effect.map((config) => (config?.transformClient ? config.transformClient(client) : client)), - ), - }) - - const streamRequest = ( - request: HttpClientRequest.HttpClientRequest, - schema: Schema.Schema, - ): Stream.Stream => { - const decodeEvent = Schema.decodeEffect(Schema.parseJson(schema)) - return httpClientOk.execute(request).pipe( - Effect.map((r) => r.stream), - Stream.unwrapScoped, - Stream.decodeText(), - Stream.pipeThroughChannel(Sse.makeChannel()), - Stream.takeWhile((event) => event.data !== "[DONE]"), - Stream.mapEffect((event) => decodeEvent(event.data)), - Stream.catchTags({ - RequestError: (error) => - AiError.HttpRequestError.fromRequestError({ - module: "OpenRouterClient", - method: "streamRequest", - error, - }), - ResponseError: (error) => - AiError.HttpResponseError.fromResponseError({ - module: "OpenRouterClient", - method: "streamRequest", - error, - }), - ParseError: (error) => - AiError.MalformedOutput.fromParseError({ - module: "OpenRouterClient", - method: "streamRequest", - error, - }), - }), - ) - } - - const createChatCompletion: ( - options: typeof Generated.ChatGenerationParams.Encoded, - ) => Effect.Effect = Effect.fnUntraced(function* (options) { - return yield* client.sendChatCompletionRequest(options).pipe( - Effect.catchTag( - "ChatError", - (error) => - new AiError.HttpResponseError({ - module: "OpenRouterClient", - method: "createChatCompletion", - reason: "StatusCode", - request: { - hash: error.request.hash, - headers: error.request.headers, - method: error.request.method, - url: error.request.url, - urlParams: error.request.urlParams, - }, - response: { - headers: error.response.headers, - status: error.response.status, - }, - }), - ), - Effect.catchTags({ - RequestError: (error) => - AiError.HttpRequestError.fromRequestError({ - module: "OpenRouterClient", - method: "createChatCompletion", - error, - }), - ResponseError: (error) => - AiError.HttpResponseError.fromResponseError({ - module: "OpenRouterClient", - method: "createChatCompletion", - error, - }), - ParseError: (error) => - AiError.MalformedOutput.fromParseError({ - module: "OpenRouterClient", - method: "createChatCompletion", - error, - }), - }), - ) - }) - - const createChatCompletionStream = ( - options: Omit, - ): Stream.Stream => { - const request = HttpClientRequest.post("/chat/completions", { - body: HttpBody.unsafeJson({ - ...options, - stream: true, - stream_options: { include_usage: true }, - }), - }) - return streamRequest(request, ChatStreamingResponseChunk) - } - - return OpenRouterClient.of({ - client, - createChatCompletion, - createChatCompletionStream, - }) -}) - -/** - * @since 1.0.0 - * @category Layers - */ -export const layer = (options: { - readonly apiKey?: Redacted.Redacted | undefined - readonly apiUrl?: string | undefined - /** - * Optional URL of your site for rankings on `openrouter.ai`. - */ - readonly referrer?: string | undefined - /** - * Optional title of your site for rankings on `openrouter.ai`. - */ - readonly title?: string | undefined - /** - * A function to transform the underlying HTTP client before it's used to send - * API requests. - * - * This transformation function receives the configured HTTP client and returns - * a modified version. It's applied after all standard client configuration - * (authentication, base URL, headers) but before any requests are made. - * - * Use this for: - * - Adding custom middleware (logging, metrics, caching) - * - Modifying request/response processing behavior - * - Adding custom retry logic or error handling - * - Integrating with monitoring or debugging tools - * - Applying organization-specific HTTP client policies - * - * The transformation is applied once during client initialization and affects - * all subsequent API requests made through this client instance. - * - * Leave absent or set to `undefined` if no custom HTTP client behavior is - * needed. - */ - readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined -}): Layer.Layer => - Layer.effect(OpenRouterClient, make(options)) - -/** - * @since 1.0.0 - * @category Layers - */ -export const layerConfig = (options: { - readonly apiKey?: Config.Config | undefined - readonly apiUrl?: Config.Config | undefined - /** - * Optional URL of your site for rankings on `openrouter.ai`. - */ - readonly referrer?: Config.Config | undefined - /** - * Optional title of your site for rankings on `openrouter.ai`. - */ - readonly title?: Config.Config | undefined - /** - * A function to transform the underlying HTTP client before it's used to send - * API requests. - * - * This transformation function receives the configured HTTP client and returns - * a modified version. It's applied after all standard client configuration - * (authentication, base URL, headers) but before any requests are made. - * - * Use this for: - * - Adding custom middleware (logging, metrics, caching) - * - Modifying request/response processing behavior - * - Adding custom retry logic or error handling - * - Integrating with monitoring or debugging tools - * - Applying organization-specific HTTP client policies - * - * The transformation is applied once during client initialization and affects - * all subsequent API requests made through this client instance. - * - * Leave absent or set to `undefined` if no custom HTTP client behavior is - * needed. - */ - readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined -}): Layer.Layer => { - const { transformClient, ...configs } = options - return Config.all(configs).pipe( - Effect.flatMap((configs) => make({ ...configs, transformClient })), - Layer.effect(OpenRouterClient), - ) -} - -/** - * @since 1.0.0 - * @category Schemas - */ -export class ChatStreamingMessageToolCall extends Schema.Class( - "@effect/ai-openrouter/ChatStreamingMessageToolCall", -)({ - index: Schema.Number, - id: Schema.optional(Schema.NullOr(Schema.String)), - type: Schema.Literal("function"), - function: Schema.Struct({ - name: Schema.optional(Schema.NullOr(Schema.String)), - arguments: Schema.String, - }), -}) {} - -/** - * @since 1.0.0 - * @category Schemas - */ -export class ChatStreamingMessageChunk extends Schema.Class( - "@effect/ai-openrouter/ChatStreamingMessageChunk", -)({ - role: Schema.optional(Schema.NullOr(Schema.Literal("assistant"))), - content: Schema.optional(Schema.NullOr(Schema.String)), - reasoning: Schema.optional(Schema.NullOr(Schema.String)), - reasoning_details: Schema.optional(Schema.NullOr(Schema.Array(Generated.ReasoningDetail))), - images: Schema.optional(Schema.NullOr(Schema.Array(Generated.ChatMessageContentItemImage))), - refusal: Schema.optional(Schema.NullOr(Schema.String)), - tool_calls: Schema.optional(Schema.NullOr(Schema.Array(ChatStreamingMessageToolCall))), - annotations: Schema.optional(Schema.NullOr(Schema.Array(Generated.AnnotationDetail))), -}) {} - -/** - * @since 1.0.0 - * @category Schemas - */ -export class ChatStreamingChoice extends Schema.Class( - "@effect/ai-openrouter/ChatStreamingChoice", -)({ - index: Schema.Number, - delta: Schema.optional(Schema.NullOr(ChatStreamingMessageChunk)), - finish_reason: Schema.optional(Schema.NullOr(Generated.ChatCompletionFinishReason)), - native_finish_reason: Schema.optional(Schema.NullOr(Schema.String)), - logprobs: Schema.optional(Schema.NullOr(Generated.ChatMessageTokenLogprobs)), -}) {} - -/** - * @since 1.0.0 - * @category Schemas - */ -export class ChatStreamingResponseChunk extends Schema.Class( - "@effect/ai-openrouter/ChatStreamingResponseChunk", -)({ - id: Schema.optional(Schema.NullOr(Schema.String)), - model: Schema.optionalWith(Schema.TemplateLiteral(Schema.String, Schema.Literal("/"), Schema.String), { - nullable: true, - }), - provider: Schema.optional(Schema.NullOr(Schema.String)), - created: Schema.DateTimeUtcFromNumber, - choices: Schema.Array(ChatStreamingChoice), - error: Schema.optional(Schema.NullOr(Generated.ChatError.fields.error)), - system_fingerprint: Schema.optional(Schema.NullOr(Schema.String)), - usage: Schema.optional(Schema.NullOr(Generated.ChatGenerationTokenUsage)), -}) {} diff --git a/libs/ai-openrouter/src/OpenRouterConfig.ts b/libs/ai-openrouter/src/OpenRouterConfig.ts deleted file mode 100644 index 3c9552122..000000000 --- a/libs/ai-openrouter/src/OpenRouterConfig.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @since 1.0.0 - */ -import type { HttpClient } from "@effect/platform/HttpClient" -import * as Effect from "effect/Effect" -import { dual } from "effect/Function" -import * as ServiceMap from "effect/ServiceMap" - -/** - * @since 1.0.0 - * @category Context - */ -export class OpenRouterConfig extends ServiceMap.Service()("@effect/ai-openrouter/OpenRouterConfig") { - /** - * @since 1.0.0 - */ - static readonly getOrUndefined: Effect.Effect = Effect.map( - Effect.context(), - (context) => context.unsafeMap.get(OpenRouterConfig.key), - ) -} - -/** - * @since 1.0.0 - */ -export declare namespace OpenRouterConfig { - /** - * @since 1.0.0 - * @category Models - */ - export interface Service { - readonly transformClient?: (client: HttpClient) => HttpClient - } -} - -/** - * @since 1.0.0 - * @category Configuration - */ -export const withClientTransform: { - /** - * @since 1.0.0 - * @category Configuration - */ - ( - transform: (client: HttpClient) => HttpClient, - ): (self: Effect.Effect) => Effect.Effect - /** - * @since 1.0.0 - * @category Configuration - */ - ( - self: Effect.Effect, - transform: (client: HttpClient) => HttpClient, - ): Effect.Effect -} = dual< - /** - * @since 1.0.0 - * @category Configuration - */ - ( - transform: (client: HttpClient) => HttpClient, - ) => (self: Effect.Effect) => Effect.Effect, - /** - * @since 1.0.0 - * @category Configuration - */ - ( - self: Effect.Effect, - transform: (client: HttpClient) => HttpClient, - ) => Effect.Effect ->(2, (self, transformClient) => - Effect.flatMap(OpenRouterConfig.getOrUndefined, (config) => - Effect.provideService(self, OpenRouterConfig, { ...config, transformClient }), - ), -) diff --git a/libs/ai-openrouter/src/OpenRouterLanguageModel.ts b/libs/ai-openrouter/src/OpenRouterLanguageModel.ts deleted file mode 100644 index c095d06b6..000000000 --- a/libs/ai-openrouter/src/OpenRouterLanguageModel.ts +++ /dev/null @@ -1,1177 +0,0 @@ -/** - * @since 1.0.0 - */ -import * as AiError from "@effect/ai/AiError" -import * as LanguageModel from "@effect/ai/LanguageModel" -import * as AiModel from "@effect/ai/Model" -import type * as Prompt from "@effect/ai/Prompt" -import type * as Response from "@effect/ai/Response" -import { addGenAIAnnotations } from "@effect/ai/Telemetry" -import * as Tool from "@effect/ai/Tool" -import * as Arr from "effect/Array" -import * as DateTime from "effect/DateTime" -import * as Effect from "effect/Effect" -import * as Encoding from "effect/Encoding" -import { dual } from "effect/Function" -import * as Layer from "effect/Layer" -import * as Predicate from "effect/Predicate" -import * as ServiceMap from "effect/ServiceMap" -import * as Stream from "effect/Stream" -import type { Span } from "effect/Tracer" -import type { Simplify } from "effect/Types" -import type * as Generated from "./Generated.js" -import * as InternalUtilities from "./internal/utilities.js" -import type { ChatStreamingResponseChunk } from "./OpenRouterClient.js" -import { OpenRouterClient } from "./OpenRouterClient.js" - -// ============================================================================= -// Configuration -// ============================================================================= - -/** - * @since 1.0.0 - * @category Context - */ -export class Config extends ServiceMap.Service()("@effect/ai-openrouter/OpenRouterLanguageModel/Config") { - /** - * @since 1.0.0 - */ - static readonly getOrUndefined: Effect.Effect = Effect.map( - Effect.context(), - (context) => context.unsafeMap.get(Config.key), - ) -} - -/** - * @since 1.0.0 - */ -export declare namespace Config { - /** - * @since 1.0.0 - * @category Configuration - */ - export interface Service extends Simplify< - Partial< - Omit< - typeof Generated.ChatGenerationParams.Encoded, - "messages" | "response_format" | "tools" | "tool_choice" | "stream" - > - > - > {} -} - -// ============================================================================= -// OpenRouter Provider Options / Metadata -// ============================================================================= - -/** - * @since 1.0.0 - * @category Provider Metadata - */ -export type OpenRouterReasoningInfo = - | { - readonly type: "reasoning" - readonly signature: string | undefined - } - | { - readonly type: "encrypted_reasoning" - readonly format: (typeof Generated.ReasoningDetailSummary.Type)["format"] - readonly redactedData: string - } - -/** - * @since 1.0.0 - * @category Provider Options - */ -declare module "@effect/ai/Prompt" { - export interface SystemMessageOptions extends ProviderOptions { - readonly openrouter?: - | { - /** - * A breakpoint which marks the end of reusable content eligible for caching. - */ - readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined - } - | undefined - } - - export interface UserMessageOptions extends ProviderOptions { - readonly openrouter?: - | { - /** - * A breakpoint which marks the end of reusable content eligible for caching. - */ - readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined - } - | undefined - } - - export interface AssistantMessageOptions extends ProviderOptions { - readonly openrouter?: - | { - /** - * A breakpoint which marks the end of reusable content eligible for caching. - */ - readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined - } - | undefined - } - - export interface ToolMessageOptions extends ProviderOptions { - readonly openrouter?: - | { - /** - * A breakpoint which marks the end of reusable content eligible for caching. - */ - readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined - } - | undefined - } - - export interface TextPartOptions extends ProviderOptions { - readonly openrouter?: - | { - /** - * A breakpoint which marks the end of reusable content eligible for caching. - */ - readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined - } - | undefined - } - - export interface ReasoningPartOptions extends ProviderOptions { - readonly openrouter?: - | { - /** - * A breakpoint which marks the end of reusable content eligible for caching. - */ - readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined - } - | undefined - } - - export interface FilePartOptions extends ProviderOptions { - readonly openrouter?: - | { - /** - * The name to give to the file. Will be prioritized over the file name - * associated with the file part, if present. - */ - readonly fileName?: string | undefined - /** - * A breakpoint which marks the end of reusable content eligible for caching. - */ - readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined - } - | undefined - } - - export interface ToolResultPartOptions extends ProviderOptions { - readonly openrouter?: - | { - /** - * A breakpoint which marks the end of reusable content eligible for caching. - */ - readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined - } - | undefined - } -} - -/** - * @since 1.0.0 - * @category Provider Metadata - */ -declare module "@effect/ai/Response" { - export interface ReasoningPartMetadata extends ProviderMetadata { - readonly openrouter?: OpenRouterReasoningInfo | undefined - } - - export interface ReasoningStartPartMetadata extends ProviderMetadata { - readonly openrouter?: OpenRouterReasoningInfo | undefined - } - - export interface ReasoningDeltaPartMetadata extends ProviderMetadata { - readonly openrouter?: OpenRouterReasoningInfo | undefined - } - - export interface UrlSourcePartMetadata extends ProviderMetadata { - readonly openrouter?: - | { - readonly content?: string | undefined - } - | undefined - } - - export interface FinishPartMetadata extends ProviderMetadata { - readonly openrouter?: - | { - /** - * The provider used to generate the response. - */ - readonly provider?: string | undefined - /** - * Additional usage information. - */ - readonly usage?: - | { - /** - * The total cost of generating the response. - */ - readonly cost?: number | undefined - /** - * Additional details about cost. - */ - readonly costDetails?: - | { - readonly upstream_inference_cost?: number | undefined - } - | undefined - /** - * Additional details about prompt token usage. - */ - readonly promptTokensDetails?: { - readonly audio_tokens?: number | undefined - readonly cached_tokens?: number | undefined - } - /** - * Additional details about completion token usage. - */ - readonly completionTokensDetails?: - | { - readonly reasoning_tokens?: number | undefined - readonly audio_tokens?: number | undefined - readonly accepted_prediction_tokens?: number | undefined - readonly rejected_prediction_tokens?: number | undefined - } - | undefined - } - | undefined - } - | undefined - } -} - -// ============================================================================= -// OpenRouter Language Model -// ============================================================================= - -/** - * @since 1.0.0 - * @category Ai Models - */ -export const model = ( - model: string, - config?: Omit, -): AiModel.Model<"openrouter", LanguageModel.LanguageModel, OpenRouterClient> => - AiModel.make("openrouter", layer({ model, config })) - -/** - * @since 1.0.0 - * @category Constructors - */ -export const make = Effect.fnUntraced(function* (options: { - readonly model: string - readonly config?: Omit -}) { - const client = yield* OpenRouterClient - - const makeRequest = Effect.fnUntraced(function* (providerOptions: LanguageModel.ProviderOptions) { - const context = yield* Effect.context() - const config = { model: options.model, ...options.config, ...context.unsafeMap.get(Config.key) } - const messages = yield* prepareMessages(providerOptions) - const { toolChoice, tools } = yield* prepareTools(providerOptions) - const responseFormat = providerOptions.responseFormat - const request: typeof Generated.ChatGenerationParams.Encoded = { - ...config, - messages, - tools, - tool_choice: toolChoice, - response_format: - responseFormat.type === "text" - ? undefined - : { - type: "json_schema", - json_schema: { - name: responseFormat.objectName, - description: - Tool.getDescriptionFromSchemaAst(responseFormat.schema.ast) ?? - "Respond with a JSON object", - schema: Tool.getJsonSchemaFromSchemaAst(responseFormat.schema.ast), - strict: true, - }, - }, - } - return request - }) - - return yield* LanguageModel.make({ - generateText: Effect.fnUntraced(function* (options) { - const request = yield* makeRequest(options) - annotateRequest(options.span, request) - const rawResponse = yield* client.createChatCompletion(request) - annotateResponse(options.span, rawResponse) - return yield* makeResponse(rawResponse) - }), - streamText: Effect.fnUntraced( - function* (options) { - const request = yield* makeRequest(options) - annotateRequest(options.span, request) - return client.createChatCompletionStream(request) - }, - (effect, options) => - effect.pipe( - Effect.flatMap((stream) => makeStreamResponse(stream)), - Stream.unwrap, - Stream.map((response) => { - annotateStreamResponse(options.span, response) - return response - }), - ), - ), - }) -}) - -/** - * @since 1.0.0 - * @category Layers - */ -export const layer = (options: { - readonly model: string - readonly config?: Omit -}): Layer.Layer => - Layer.effect(LanguageModel.LanguageModel, make({ model: options.model, config: options.config })) - -/** - * @since 1.0.0 - * @category Configuration - */ -export const withConfigOverride: { - /** - * @since 1.0.0 - * @category Configuration - */ - (config: Config.Service): (self: Effect.Effect) => Effect.Effect - /** - * @since 1.0.0 - * @category Configuration - */ - (self: Effect.Effect, config: Config.Service): Effect.Effect -} = dual< - /** - * @since 1.0.0 - * @category Configuration - */ - (config: Config.Service) => (self: Effect.Effect) => Effect.Effect, - /** - * @since 1.0.0 - * @category Configuration - */ - (self: Effect.Effect, config: Config.Service) => Effect.Effect ->(2, (self, overrides) => - Effect.flatMap(Config.getOrUndefined, (config) => - Effect.provideService(self, Config, { ...config, ...overrides }), - ), -) - -// ============================================================================= -// Prompt Conversion -// ============================================================================= - -const prepareMessages: ( - options: LanguageModel.ProviderOptions, -) => Effect.Effect, AiError.AiError> = Effect.fnUntraced( - function* (options) { - const messages: Array = [] - - for (const message of options.prompt.content) { - switch (message.role) { - case "system": { - messages.push({ - role: "system", - content: message.content, - cache_control: getCacheControl(message), - }) - break - } - - case "user": { - const firstPart = message.content[0] - if (message.content.length === 1 && firstPart?.type === "text") { - const cacheControl = getCacheControl(message) ?? getCacheControl(firstPart) - messages.push({ - role: "user", - content: Predicate.isNotUndefined(cacheControl) - ? [{ type: "text", text: firstPart.text, cache_control: cacheControl }] - : firstPart.text, - }) - } else { - const content: Array = [] - const messageCacheControl = getCacheControl(message) - for (const part of message.content) { - const partCacheControl = getCacheControl(part) - const cacheControl = partCacheControl ?? messageCacheControl - switch (part.type) { - case "text": { - content.push({ - type: "text", - text: part.text, - cache_control: cacheControl, - }) - break - } - case "file": { - if (part.mediaType.startsWith("image/")) { - const mediaType = - part.mediaType === "image/*" ? "image/jpeg" : part.mediaType - content.push({ - type: "image_url", - image_url: { - url: - part.data instanceof URL - ? part.data.toString() - : part.data instanceof Uint8Array - ? `data:${mediaType};base64,${Encoding.encodeBase64(part.data)}` - : part.data, - }, - cache_control: cacheControl, - }) - } else { - const options = part.options.openrouter - const fileName = options?.fileName ?? part.fileName ?? "" - content.push({ - type: "file", - file: { - filename: fileName, - file_data: - part.data instanceof URL - ? part.data.toString() - : part.data instanceof Uint8Array - ? `data:${part.mediaType};base64,${Encoding.encodeBase64(part.data)}` - : part.data, - }, - cache_control: - part.data instanceof URL ? cacheControl : undefined, - }) - } - break - } - } - } - messages.push({ - role: "user", - content, - }) - } - break - } - - case "assistant": { - let text = "" - let reasoning = "" - const reasoningDetails: Array = [] - const toolCalls: Array = [] - const cacheControl = getCacheControl(message) - for (const part of message.content) { - switch (part.type) { - case "text": { - text += part.text - break - } - case "reasoning": { - reasoning += part.text - reasoningDetails.push({ - type: "reasoning.text", - text: part.text, - }) - break - } - case "tool-call": { - toolCalls.push({ - id: part.id, - type: "function", - function: { - name: part.name, - arguments: JSON.stringify(part.params), - }, - }) - break - } - } - } - messages.push({ - role: "assistant", - content: text, - tool_calls: toolCalls.length > 0 ? toolCalls : undefined, - reasoning: reasoning.length > 0 ? reasoning : undefined, - reasoning_details: reasoningDetails.length > 0 ? reasoningDetails : undefined, - cache_control: cacheControl, - }) - break - } - - case "tool": { - const cacheControl = getCacheControl(message) - for (const part of message.content) { - messages.push({ - role: "tool", - tool_call_id: part.id, - content: JSON.stringify(part.result), - cache_control: cacheControl, - }) - } - break - } - } - } - - return messages - }, -) - -// ============================================================================= -// Tool Conversion -// ============================================================================= - -const prepareTools: (options: LanguageModel.ProviderOptions) => Effect.Effect< - { - readonly tools: ReadonlyArray | undefined - readonly toolChoice: typeof Generated.ToolChoiceOption.Encoded | undefined - }, - AiError.AiError -> = Effect.fnUntraced(function* (options: LanguageModel.ProviderOptions) { - if (options.tools.length === 0) { - return { tools: undefined, toolChoice: undefined } - } - - const hasProviderDefinedTools = options.tools.some((tool) => Tool.isProviderDefined(tool)) - if (hasProviderDefinedTools) { - return yield* new AiError.MalformedInput({ - module: "OpenRouterLanguageModel", - method: "prepareTools", - description: - "Provider-defined tools are unsupported by the OpenRouter " + - "provider integration at this time", - }) - } - - let tools: Array = [] - let toolChoice: typeof Generated.ToolChoiceOption.Encoded | undefined = undefined - - for (const tool of options.tools) { - tools.push({ - type: "function", - function: { - name: tool.name, - description: Tool.getDescription(tool as any), - parameters: Tool.getJsonSchema(tool as any) as any, - strict: true, - }, - }) - } - - if (options.toolChoice === "none") { - toolChoice = "none" - } else if (options.toolChoice === "auto") { - toolChoice = "auto" - } else if (options.toolChoice === "required") { - toolChoice = "required" - } else if ("tool" in options.toolChoice) { - toolChoice = { type: "function", function: { name: options.toolChoice.tool } } - } else { - const allowedTools = new Set(options.toolChoice.oneOf) - tools = tools.filter((tool) => allowedTools.has(tool.function.name)) - toolChoice = options.toolChoice.mode === "auto" ? "auto" : "required" - } - - return { tools, toolChoice } -}) - -// ============================================================================= -// Response Conversion -// ============================================================================= - -const makeResponse: ( - response: Generated.ChatResponse, -) => Effect.Effect, AiError.AiError> = Effect.fnUntraced(function* (response) { - const choice = response.choices[0] - - if (Predicate.isUndefined(choice)) { - return yield* new AiError.MalformedOutput({ - module: "OpenRouterLanguageModel", - method: "makeResponse", - description: "Received response with no valid choices", - }) - } - - const parts: Array = [] - const message = choice.message - - const createdAt = new Date(response.created * 1000) - parts.push({ - type: "response-metadata", - id: response.id, - modelId: response.model, - timestamp: DateTime.formatIso(DateTime.unsafeFromDate(createdAt)), - }) - - if (Predicate.isNotNullable(message.reasoning) && message.reasoning.length > 0) { - parts.push({ - type: "reasoning", - text: message.reasoning, - }) - } - - if (Predicate.isNotNullable(message.reasoning_details) && message.reasoning_details.length > 0) { - for (const detail of message.reasoning_details) { - switch (detail.type) { - case "reasoning.summary": { - if (Predicate.isNotUndefined(detail.summary) && detail.summary.length > 0) { - parts.push({ - type: "reasoning", - text: detail.summary, - }) - } - break - } - case "reasoning.encrypted": { - if (Predicate.isNotUndefined(detail.data) && detail.data.length > 0) { - parts.push({ - type: "reasoning", - text: "", - metadata: { - openrouter: { - type: "encrypted_reasoning", - format: detail.format, - redactedData: detail.data, - }, - }, - }) - } - break - } - case "reasoning.text": { - if (Predicate.isNotUndefined(detail.text) && detail.text.length > 0) { - parts.push({ - type: "reasoning", - text: detail.text, - metadata: { - openrouter: { - type: "reasoning", - signature: detail.signature, - }, - }, - }) - } - break - } - } - } - } - - if (Predicate.isNotNullable(message.content) && message.content.length > 0) { - parts.push({ - type: "text", - text: message.content as string, - }) - } - - if (Predicate.isNotNullable(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - const toolName = toolCall.function.name - const toolParams = toolCall.function.arguments - const params = yield* Effect.try({ - try: () => Tool.unsafeSecureJsonParse(toolParams), - catch: (cause) => - new AiError.MalformedOutput({ - module: "OpenRouterLanguageModel", - method: "makeResponse", - description: - "Failed to securely parse tool call parameters " + - `for tool '${toolName}':\nParameters: ${toolParams}`, - cause, - }), - }) - parts.push({ - type: "tool-call", - id: toolCall.id, - name: toolName, - params, - }) - } - } - - if (Predicate.isNotNullable(message.annotations)) { - for (const annotation of message.annotations) { - if (annotation.type === "url_citation") { - parts.push({ - type: "source", - sourceType: "url", - id: annotation.url_citation.url, - url: annotation.url_citation.url, - title: annotation.url_citation.title, - metadata: { - openrouter: { - content: annotation.url_citation.content, - }, - }, - }) - } - } - } - - if (Predicate.isNotNullable(message.images)) { - for (const image of message.images) { - parts.push({ - type: "file", - mediaType: getMediaType(image.image_url.url) ?? "image/jpeg", - data: getBase64FromDataUrl(image.image_url.url), - }) - } - } - - parts.push({ - type: "finish", - reason: InternalUtilities.resolveFinishReason(choice.finish_reason), - usage: { - inputTokens: response.usage?.prompt_tokens, - outputTokens: response.usage?.completion_tokens, - totalTokens: response.usage?.total_tokens, - reasoningTokens: response.usage?.completion_tokens_details?.reasoning_tokens, - cachedInputTokens: response.usage?.prompt_tokens_details?.cached_tokens, - }, - metadata: { - openrouter: { - provider: response.provider, - usage: { - cost: response.usage?.cost, - promptTokensDetails: response.usage?.prompt_tokens_details, - completionTokensDetails: response.usage?.completion_tokens_details, - costDetails: response.usage?.cost_details, - }, - }, - }, - }) - - return parts -}) - -const makeStreamResponse: ( - stream: Stream.Stream, -) => Effect.Effect> = Effect.fnUntraced( - function* (stream) { - let idCounter = 0 - let activeTextId: string | undefined = undefined - let activeReasoningId: string | undefined = undefined - let finishReason: Response.FinishReason = "unknown" - let responseMetadataEmitted = false - - const activeToolCalls: Record< - number, - { - readonly index: number - readonly id: string - readonly name: string - params: string - } - > = {} - - return stream.pipe( - Stream.mapEffect( - Effect.fnUntraced(function* (event) { - const parts: Array = [] - - if ("error" in event) { - parts.push({ - type: "error", - error: event.error, - }) - return parts - } - - // Response Metadata - - if (Predicate.isNotUndefined(event.id) && !responseMetadataEmitted) { - parts.push({ - type: "response-metadata", - id: event.id, - modelId: event.model, - timestamp: DateTime.formatIso(yield* DateTime.now), - }) - responseMetadataEmitted = true - } - - const choice = event.choices[0] - - if (Predicate.isUndefined(choice)) { - // Usage-only chunk from stream_options.include_usage — no content to emit - return parts - } - - const delta = choice.delta - - if (Predicate.isUndefined(delta)) { - return parts - } - - // Reasoning Parts - - const emitReasoningPart = ( - delta: string, - metadata: OpenRouterReasoningInfo | undefined = undefined, - ) => { - // End in-progress text part if present before starting reasoning - if (Predicate.isNotUndefined(activeTextId)) { - parts.push({ - type: "text-end", - id: activeTextId, - }) - activeTextId = undefined - } - // Start a new reasoning part if necessary - if (Predicate.isUndefined(activeReasoningId)) { - activeReasoningId = (idCounter++).toString() - parts.push({ - type: "reasoning-start", - id: activeReasoningId, - metadata: { openrouter: metadata }, - }) - } - // Emit the reasoning delta - parts.push({ - type: "reasoning-delta", - id: activeReasoningId, - delta, - metadata: { openrouter: metadata }, - }) - } - - if ( - Predicate.isNotNullable(delta.reasoning_details) && - delta.reasoning_details.length > 0 - ) { - for (const detail of delta.reasoning_details) { - switch (detail.type) { - case "reasoning.summary": { - if ( - Predicate.isNotUndefined(detail.summary) && - detail.summary.length > 0 - ) { - emitReasoningPart(detail.summary) - } - break - } - case "reasoning.encrypted": { - if (Predicate.isNotUndefined(detail.data) && detail.data.length > 0) { - emitReasoningPart("", { - type: "encrypted_reasoning", - format: detail.format, - redactedData: detail.data, - }) - } - break - } - case "reasoning.text": { - if (Predicate.isNotUndefined(detail.text) && detail.text.length > 0) { - emitReasoningPart(detail.text, { - type: "reasoning", - signature: detail.signature, - }) - } - break - } - } - } - } else if (Predicate.isNotNullable(delta.reasoning) && delta.reasoning.length > 0) { - emitReasoningPart(delta.reasoning) - } - - // Text Parts - - if (Predicate.isNotNullable(delta.content) && delta.content.length > 0) { - // End in-progress reasoning part if present before starting text - if (Predicate.isNotUndefined(activeReasoningId)) { - parts.push({ - type: "reasoning-end", - id: activeReasoningId, - }) - activeReasoningId = undefined - } - // Start a new text part if necessary - if (Predicate.isUndefined(activeTextId)) { - activeTextId = (idCounter++).toString() - parts.push({ - type: "text-start", - id: activeTextId, - }) - } - // Emit the text delta - parts.push({ - type: "text-delta", - id: activeTextId, - delta: delta.content, - }) - } - - // Source Parts - - if (Predicate.isNotNullable(delta.annotations)) { - for (const annotation of delta.annotations) { - if (annotation.type === "url_citation") { - parts.push({ - type: "source", - sourceType: "url", - id: annotation.url_citation.url, - url: annotation.url_citation.url, - title: annotation.url_citation.title, - metadata: { - openrouter: { - content: annotation.url_citation.content, - }, - }, - }) - } - } - } - - // Tool Call Parts - - if (Predicate.isNotNullable(delta.tool_calls) && delta.tool_calls.length > 0) { - for (const toolCall of delta.tool_calls) { - // Get the active tool call, if present - let activeToolCall = activeToolCalls[toolCall.index] - - // If no active tool call was found, start a new active tool call - if (Predicate.isUndefined(activeToolCall)) { - // The tool call id and function name always come back with the - // first tool call delta - activeToolCall = { - index: toolCall.index, - id: toolCall.id!, - name: toolCall.function.name!, - params: toolCall.function.arguments ?? "", - } - - activeToolCalls[toolCall.index] = activeToolCall - - parts.push({ - type: "tool-params-start", - id: activeToolCall.id, - name: activeToolCall.name, - }) - - // Emit a tool call delta part if parameters were also sent - if (activeToolCall.params.length > 0) { - parts.push({ - type: "tool-params-delta", - id: activeToolCall.id, - delta: activeToolCall.params, - }) - } - } else { - // If an active tool call was found, update and emit the delta for - // the tool call's parameters - activeToolCall.params += toolCall.function.arguments - parts.push({ - type: "tool-params-delta", - id: activeToolCall.id, - delta: activeToolCall.params, - }) - } - - // Check if the tool call is complete - try { - const params = Tool.unsafeSecureJsonParse(activeToolCall.params) - parts.push({ - type: "tool-params-end", - id: activeToolCall.id, - }) - parts.push({ - type: "tool-call", - id: activeToolCall.id, - name: activeToolCall.name, - params, - }) - delete activeToolCalls[toolCall.index] - } catch { - // Tool call incomplete, continue parsing - continue - } - } - } - - // File Parts - - if (Predicate.isNotNullable(delta.images)) { - for (const image of delta.images) { - parts.push({ - type: "file", - mediaType: getMediaType(image.image_url.url) ?? "image/jpeg", - data: getBase64FromDataUrl(image.image_url.url), - }) - } - } - - // Finish Parts - - if (Predicate.isNotNullable(choice.finish_reason)) { - finishReason = InternalUtilities.resolveFinishReason(choice.finish_reason) - } - - // Usage is only emitted by the last part of the stream, so we need to - // handle flushing any remaining text / reasoning / tool calls - if (Predicate.isNotUndefined(event.usage)) { - // Complete any remaining tool calls if the finish reason is tool-calls - if (finishReason === "tool-calls") { - for (const toolCall of Object.values(activeToolCalls)) { - // Coerce invalid tool call parameters to an empty object - const params = yield* Effect.try(() => - Tool.unsafeSecureJsonParse(toolCall.params), - ).pipe(Effect.catch(() => Effect.succeed({}))) - parts.push({ - type: "tool-params-end", - id: toolCall.id, - }) - parts.push({ - type: "tool-call", - id: toolCall.id, - name: toolCall.name, - params, - }) - delete activeToolCalls[toolCall.index] - } - } - - // Flush remaining reasoning parts - if (Predicate.isNotUndefined(activeReasoningId)) { - parts.push({ - type: "reasoning-end", - id: activeReasoningId, - }) - activeReasoningId = undefined - } - - // Flush remaining text parts - if (Predicate.isNotUndefined(activeTextId)) { - parts.push({ - type: "text-end", - id: activeTextId, - }) - activeTextId = undefined - } - - parts.push({ - type: "finish", - reason: finishReason, - usage: { - inputTokens: event.usage?.prompt_tokens, - outputTokens: event.usage?.completion_tokens, - totalTokens: event.usage?.total_tokens, - reasoningTokens: event.usage?.completion_tokens_details?.reasoning_tokens, - cachedInputTokens: event.usage?.prompt_tokens_details?.cached_tokens, - }, - metadata: { - openrouter: { - provider: event.provider, - usage: { - cost: event.usage?.cost, - promptTokensDetails: event.usage?.prompt_tokens_details, - completionTokensDetails: event.usage?.completion_tokens_details, - costDetails: event.usage?.cost_details, - }, - }, - }, - }) - } - - return parts - }), - ), - Stream.flattenIterables, - ) - }, -) - -// ============================================================================= -// Telemetry -// ============================================================================= - -const annotateRequest = (span: Span, request: typeof Generated.ChatGenerationParams.Encoded): void => { - addGenAIAnnotations(span, { - system: "openrouter", - operation: { name: "chat" }, - request: { - model: request.model, - temperature: request.temperature, - topP: request.top_p, - maxTokens: request.max_tokens, - stopSequences: Arr.ensure(request.stop).filter(Predicate.isNotNullable), - }, - }) -} - -const annotateResponse = (span: Span, response: Generated.ChatResponse): void => { - addGenAIAnnotations(span, { - response: { - id: response.id, - model: response.model, - finishReasons: response.choices - .map((choice) => choice.finish_reason) - .filter(Predicate.isNotNullable), - }, - usage: { - inputTokens: response.usage?.prompt_tokens, - outputTokens: response.usage?.completion_tokens, - }, - }) -} - -const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => { - if (part.type === "response-metadata") { - addGenAIAnnotations(span, { - response: { - id: part.id, - model: part.modelId, - }, - }) - } - if (part.type === "finish") { - addGenAIAnnotations(span, { - response: { - finishReasons: [part.reason], - }, - usage: { - inputTokens: part.usage.inputTokens, - outputTokens: part.usage.outputTokens, - }, - }) - } -} - -// ============================================================================= -// Utilities -// ============================================================================= - -const getCacheControl = ( - part: - | Prompt.SystemMessage - | Prompt.UserMessage - | Prompt.AssistantMessage - | Prompt.ToolMessage - | Prompt.TextPart - | Prompt.ReasoningPart - | Prompt.FilePart - | Prompt.ToolResultPart, -): typeof Generated.CacheControlEphemeral.Encoded | undefined => part.options.openrouter?.cacheControl - -const getMediaType = (dataUrl: string): string | undefined => { - const match = dataUrl.match(/^data:([^;]+)/) - return match ? match[1] : undefined -} - -const getBase64FromDataUrl = (dataUrl: string): string => { - const match = dataUrl.match(/^data:[^;]*;base64,(.+)$/) - return match ? match[1]! : dataUrl -} diff --git a/libs/ai-openrouter/src/index.ts b/libs/ai-openrouter/src/index.ts deleted file mode 100644 index 8d2a1b0eb..000000000 --- a/libs/ai-openrouter/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @since 1.0.0 - */ -export * as Generated from "./Generated.js" - -/** - * @since 1.0.0 - */ -export * as OpenRouterClient from "./OpenRouterClient.js" - -/** - * @since 1.0.0 - */ -export * as OpenRouterConfig from "./OpenRouterConfig.js" - -/** - * @since 1.0.0 - */ -export * as OpenRouterLanguageModel from "./OpenRouterLanguageModel.js" diff --git a/libs/ai-openrouter/src/internal/utilities.ts b/libs/ai-openrouter/src/internal/utilities.ts deleted file mode 100644 index 54bb0a415..000000000 --- a/libs/ai-openrouter/src/internal/utilities.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type * as Response from "@effect/ai/Response" -import * as Predicate from "effect/Predicate" -import type * as Generated from "../Generated.js" - -const finishReasonMap: Record = { - content_filter: "content-filter", - error: "error", - function_call: "tool-calls", - tool_calls: "tool-calls", - length: "length", - stop: "stop", -} - -/** @internal */ -export const resolveFinishReason = ( - finishReason: typeof Generated.ChatCompletionFinishReason.Type | null, -): Response.FinishReason => { - if (Predicate.isNull(finishReason)) { - return "unknown" - } - const reason = finishReasonMap[finishReason] - if (Predicate.isUndefined(reason)) { - return "unknown" - } - return reason -} diff --git a/libs/ai-openrouter/tsconfig.json b/libs/ai-openrouter/tsconfig.json deleted file mode 100644 index a98bb77f1..000000000 --- a/libs/ai-openrouter/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "include": ["src/**/*.ts"], - "exclude": ["**/*.test.ts", "**/*.spec.ts"], - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "lib": ["ES2022", "DOM"], - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - /* Declaration output */ - "declaration": true, - "declarationMap": true, - - /* Linting */ - "skipLibCheck": true, - "strict": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true, - "plugins": [ - { - "name": "@effect/language-service", - "reportSuggestionsAsWarningsInTsc": true - } - ] - } -} diff --git a/libs/bot-sdk/src/errors.ts b/libs/bot-sdk/src/errors.ts index d2067cf73..bca775af7 100644 --- a/libs/bot-sdk/src/errors.ts +++ b/libs/bot-sdk/src/errors.ts @@ -3,10 +3,13 @@ import { Schema } from "effect" /** * Error thrown when bot authentication fails. */ -export class AuthenticationError extends Schema.TaggedErrorClass()("AuthenticationError", { - message: Schema.String, - cause: Schema.Unknown, -}) {} +export class AuthenticationError extends Schema.TaggedErrorClass()( + "AuthenticationError", + { + message: Schema.String, + cause: Schema.Unknown, + }, +) {} /** * Error thrown when a command payload cannot be decoded. @@ -23,11 +26,14 @@ export class CommandArgsDecodeError extends Schema.TaggedErrorClass()("CommandHandlerError", { - message: Schema.String, - commandName: Schema.String, - cause: Schema.Unknown, -}) {} +export class CommandHandlerError extends Schema.TaggedErrorClass()( + "CommandHandlerError", + { + message: Schema.String, + commandName: Schema.String, + cause: Schema.Unknown, + }, +) {} /** * Error thrown when syncing slash commands with the backend fails. @@ -40,10 +46,13 @@ export class CommandSyncError extends Schema.TaggedErrorClass( /** * Error thrown when syncing mentionable settings fails. */ -export class MentionableSyncError extends Schema.TaggedErrorClass()("MentionableSyncError", { - message: Schema.String, - cause: Schema.Unknown, -}) {} +export class MentionableSyncError extends Schema.TaggedErrorClass()( + "MentionableSyncError", + { + message: Schema.String, + cause: Schema.Unknown, + }, +) {} export class GatewayReadError extends Schema.TaggedErrorClass()("GatewayReadError", { message: Schema.String, diff --git a/libs/bot-sdk/src/hazel-bot-sdk.ts b/libs/bot-sdk/src/hazel-bot-sdk.ts index 34f857511..07fcafd5a 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.ts @@ -119,7 +119,8 @@ export interface HazelBotRuntimeConfig = Comm readonly heartbeatIntervalMs?: number } -export class HazelBotRuntimeConfigTag extends ServiceMap.Service()("@hazel/bot-sdk/HazelBotRuntimeConfig") {} diff --git a/libs/bot-sdk/src/rpc/client.ts b/libs/bot-sdk/src/rpc/client.ts index cfde3050f..eeee2738c 100644 --- a/libs/bot-sdk/src/rpc/client.ts +++ b/libs/bot-sdk/src/rpc/client.ts @@ -36,15 +36,16 @@ export interface BotRpcClientConfig { /** * Internal context tag for the RPC client configuration */ -export class BotRpcClientConfigTag extends ServiceMap.Service()("@hazel/bot-sdk/BotRpcClientConfig") {} +export class BotRpcClientConfigTag extends ServiceMap.Service()( + "@hazel/bot-sdk/BotRpcClientConfig", +) {} /** * Context tag for the RPC client instance * Type is inferred from the actual RpcClient.make result */ -export class BotRpcClient extends ServiceMap.Service> >()("@hazel/bot-sdk/BotRpcClient") {} diff --git a/libs/bot-sdk/src/services/health-server.ts b/libs/bot-sdk/src/services/health-server.ts index 403e4bec5..2cae45dcb 100644 --- a/libs/bot-sdk/src/services/health-server.ts +++ b/libs/bot-sdk/src/services/health-server.ts @@ -13,7 +13,8 @@ export interface BotHealthServerConfig { readonly port: number } -export class BotHealthServerConfigTag extends ServiceMap.Service()("@hazel/bot-sdk/BotHealthServerConfig") {} diff --git a/libs/bot-sdk/src/streaming/errors.ts b/libs/bot-sdk/src/streaming/errors.ts index 599f6baec..d8373e94b 100644 --- a/libs/bot-sdk/src/streaming/errors.ts +++ b/libs/bot-sdk/src/streaming/errors.ts @@ -10,11 +10,14 @@ import { Schema } from "effect" /** * Error thrown when connecting to a message actor fails */ -export class ActorConnectionError extends Schema.TaggedErrorClass()("ActorConnectionError", { - messageId: Schema.String, - message: Schema.String, - cause: Schema.Unknown, -}) {} +export class ActorConnectionError extends Schema.TaggedErrorClass()( + "ActorConnectionError", + { + messageId: Schema.String, + message: Schema.String, + cause: Schema.Unknown, + }, +) {} /** * Error thrown when creating a message with live state fails @@ -28,11 +31,14 @@ export class MessageCreateError extends Schema.TaggedErrorClass()("ActorOperationError", { - operation: Schema.String, - message: Schema.String, - cause: Schema.Unknown, -}) {} +export class ActorOperationError extends Schema.TaggedErrorClass()( + "ActorOperationError", + { + operation: Schema.String, + message: Schema.String, + cause: Schema.Unknown, + }, +) {} /** * Error thrown when processing an async stream of chunks fails @@ -60,11 +66,14 @@ export class BotNotConfiguredError extends Schema.TaggedErrorClass()("MessagePersistError", { - messageId: Schema.String, - message: Schema.String, - cause: Schema.Unknown, -}) {} +export class MessagePersistError extends Schema.TaggedErrorClass()( + "MessagePersistError", + { + messageId: Schema.String, + message: Schema.String, + cause: Schema.Unknown, + }, +) {} /** * Union type for all streaming errors. diff --git a/packages/actors/src/auth/config-service.ts b/packages/actors/src/auth/config-service.ts index 7d140e36e..aabd1f720 100644 --- a/packages/actors/src/auth/config-service.ts +++ b/packages/actors/src/auth/config-service.ts @@ -26,18 +26,19 @@ export class TokenValidationConfigService extends ServiceMap.Service Schema.decodeUnknownEffect(WorkOSClientIdSchema)(value), + Effect.flatMap(Config.string("WORKOS_CLIENT_ID").asEffect(), (value) => + Schema.decodeUnknownEffect(WorkOSClientIdSchema)(value), ), ) const backendUrl = yield* optionalValue( - Config.string("BACKEND_URL").pipe( - Config.orElse(() => Config.string("API_BASE_URL")), - Config.orElse(() => Config.string("VITE_BACKEND_URL")), - Config.orElse(() => Config.string("VITE_API_BASE_URL")), - ).asEffect(), + Config.string("BACKEND_URL") + .pipe( + Config.orElse(() => Config.string("API_BASE_URL")), + Config.orElse(() => Config.string("VITE_BACKEND_URL")), + Config.orElse(() => Config.string("VITE_API_BASE_URL")), + ) + .asEffect(), ) const internalSecret = yield* optionalValue(Config.redacted("INTERNAL_SECRET").asEffect()) diff --git a/packages/auth/src/cache/user-lookup-cache.ts b/packages/auth/src/cache/user-lookup-cache.ts index 21e9503f7..e56ab1c4e 100644 --- a/packages/auth/src/cache/user-lookup-cache.ts +++ b/packages/auth/src/cache/user-lookup-cache.ts @@ -17,15 +17,15 @@ export const USER_LOOKUP_CACHE_PREFIX = "auth:user-lookup" export const USER_LOOKUP_CACHE_TTL = Duration.minutes(5) /** - * User lookup cache service using @effect/experimental Persistence. + * User lookup cache service using Persistence. * Caches the mapping from workosUserId (external ID) to internalUserId. * - * Uses ResultPersistence for schema-based serialization and Redis backing. - * Requires: Persistence.ResultPersistence (provided by RedisResultPersistenceLive or MemoryResultPersistenceLive) + * Uses Persistence for schema-based serialization and Redis backing. + * Requires: Persistence.Persistence (provided by Redis or Memory persistence layer) */ export class UserLookupCache extends ServiceMap.Service()("@hazel/auth/UserLookupCache", { make: Effect.gen(function* () { - const persistence = yield* Persistence.ResultPersistence + const persistence = yield* Persistence.Persistence const store = yield* persistence.make({ storeId: USER_LOOKUP_CACHE_PREFIX, @@ -58,21 +58,21 @@ export class UserLookupCache extends ServiceMap.Service()("@haz // Record latency yield* Metric.update(userLookupCacheOperationLatency, Date.now() - startTime) - if (Option.isNone(cached)) { - yield* Metric.increment(userLookupCacheMisses) + if (cached === undefined) { + yield* Metric.update(userLookupCacheMisses, 1) yield* Effect.annotateCurrentSpan("cache.result", "miss") return Option.none() } // Exit contains Success or Failure - if (cached.value._tag === "Success") { - yield* Metric.increment(userLookupCacheHits) + if (Exit.isSuccess(cached)) { + yield* Metric.update(userLookupCacheHits, 1) yield* Effect.annotateCurrentSpan("cache.result", "hit") - return Option.some(cached.value.value) + return Option.some(cached.value) } // Cached a failure - treat as cache miss - yield* Metric.increment(userLookupCacheMisses) + yield* Metric.update(userLookupCacheMisses, 1) yield* Effect.annotateCurrentSpan("cache.result", "miss") yield* Effect.annotateCurrentSpan("cache.skip_reason", "failure_cached") return Option.none() @@ -142,9 +142,10 @@ export class UserLookupCache extends ServiceMap.Service()("@haz } }), }) { + static readonly layer = Layer.effect(this, this.make) + /** Test layer that always returns cache miss */ static Test = Layer.mock(this, { - _tag: "@hazel/auth/UserLookupCache", get: (_workosUserId: WorkOSUserId) => Effect.succeed(Option.none()), set: (_workosUserId: WorkOSUserId, _internalUserId: UserId) => Effect.void, invalidate: (_workosUserId: WorkOSUserId) => Effect.void, @@ -153,7 +154,6 @@ export class UserLookupCache extends ServiceMap.Service()("@haz /** Test layer factory for configurable cache behavior */ static TestWith = (options: { cachedResult?: UserLookupResult }) => Layer.mock(UserLookupCache, { - _tag: "@hazel/auth/UserLookupCache", get: (_workosUserId: WorkOSUserId) => Effect.succeed(options.cachedResult ? Option.some(options.cachedResult) : Option.none()), set: (_workosUserId: WorkOSUserId, _internalUserId: UserId) => Effect.void, diff --git a/packages/auth/src/cache/user-lookup-request.ts b/packages/auth/src/cache/user-lookup-request.ts index b0fc5596a..602843618 100644 --- a/packages/auth/src/cache/user-lookup-request.ts +++ b/packages/auth/src/cache/user-lookup-request.ts @@ -1,5 +1,6 @@ import { UserId, WorkOSUserId } from "@hazel/schema" -import { PrimaryKey, Schema } from "effect" +import { Schema } from "effect" +import { Persistable } from "effect/unstable/persistence" import { UserLookupCacheError } from "../errors.ts" /** @@ -14,24 +15,15 @@ export type UserLookupResult = typeof UserLookupResult.Type /** * Request type for user lookup cache operations. - * Implements TaggedRequest for use with @effect/experimental Persistence. + * Implements Persistable.Class for use with Persistence. */ -export class UserLookupCacheRequest extends Schema.TaggedRequest()( - "UserLookupCacheRequest", - { - failure: UserLookupCacheError, - success: UserLookupResult, - payload: { - /** WorkOS user ID (external ID) */ - workosUserId: WorkOSUserId, - }, - }, -) { - /** - * Primary key for cache storage. - * Used by ResultPersistence to generate the cache key. - */ - [PrimaryKey.symbol]() { - return this.workosUserId +export class UserLookupCacheRequest extends Persistable.Class<{ + payload: { + /** WorkOS user ID (external ID) */ + workosUserId: typeof WorkOSUserId.Type } -} +}>()("UserLookupCacheRequest", { + primaryKey: (payload) => payload.workosUserId, + success: UserLookupResult, + error: UserLookupCacheError, +}) {} diff --git a/packages/auth/src/config.ts b/packages/auth/src/config.ts index 4e1b7b322..b50d3adb5 100644 --- a/packages/auth/src/config.ts +++ b/packages/auth/src/config.ts @@ -26,8 +26,9 @@ export class AuthConfig extends ServiceMap.Service()("@hazel/auth/Au } satisfies AuthConfigShape }), }) { + static readonly layer = Layer.effect(this, this.make) + static Test = Layer.mock(this, { - _tag: "@hazel/auth/AuthConfig", workosApiKey: "sk_test_123", workosClientId: "client_test_123", }) diff --git a/packages/auth/src/consumers/backend-auth.ts b/packages/auth/src/consumers/backend-auth.ts index 7b4b612a1..48b810b2e 100644 --- a/packages/auth/src/consumers/backend-auth.ts +++ b/packages/auth/src/consumers/backend-auth.ts @@ -77,10 +77,10 @@ export interface UserRepoLike { > } -export const decodeWorkOSJwtClaims = Schema.decodeUnknown(WorkOSJwtClaims) +export const decodeWorkOSJwtClaims = Schema.decodeUnknownEffect(WorkOSJwtClaims) export const decodeInternalOrganizationIdFromWorkOS = (externalId: string) => - Schema.decodeUnknown(OrganizationId)(externalId) + Schema.decodeUnknownEffect(OrganizationId)(externalId) /** * Backend authentication service. @@ -91,7 +91,7 @@ export const decodeInternalOrganizationIdFromWorkOS = (externalId: string) => export class BackendAuth extends ServiceMap.Service()("@hazel/auth/BackendAuth", { make: Effect.gen(function* () { const workos = yield* WorkOSClient - const clientId = yield* Config.string("WORKOS_CLIENT_ID").pipe(Effect.orDie) + const clientId = yield* Config.string("WORKOS_CLIENT_ID") const decodeClaims = decodeWorkOSJwtClaims /** @@ -276,7 +276,7 @@ export class BackendAuth extends ServiceMap.Service()("@hazel/auth/ }) const { payload } = yield* verifyWithIssuer("https://api.workos.com").pipe( - Effect.orElse(() => + Effect.catch(() => verifyWithIssuer(`https://api.workos.com/user_management/${clientId}`), ), ) @@ -350,9 +350,7 @@ export class BackendAuth extends ServiceMap.Service()("@hazel/auth/ } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(WorkOSClient.layer), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(WorkOSClient.layer)) /** Mock user ID - a valid UUID */ static readonly mockUserId = "00000000-0000-0000-0000-000000000001" as UserId @@ -386,7 +384,6 @@ export class BackendAuth extends ServiceMap.Service()("@hazel/auth/ /** Test layer with successful authentication */ static Test = Layer.mock(this, { - _tag: "@hazel/auth/BackendAuth", authenticateWithBearer: (_bearerToken: string, _userRepo: UserRepoLike) => Effect.succeed(BackendAuth.mockCurrentUser()), syncUserFromWorkOS: ( @@ -407,7 +404,6 @@ export class BackendAuth extends ServiceMap.Service()("@hazel/auth/ } }) => Layer.mock(BackendAuth, { - _tag: "@hazel/auth/BackendAuth", authenticateWithBearer: (_bearerToken: string, _userRepo: UserRepoLike) => options.shouldFail?.authenticateWithBearer ?? Effect.succeed(options.currentUser ?? BackendAuth.mockCurrentUser()), diff --git a/packages/auth/src/consumers/proxy-auth.ts b/packages/auth/src/consumers/proxy-auth.ts index 4085741f8..500cee02b 100644 --- a/packages/auth/src/consumers/proxy-auth.ts +++ b/packages/auth/src/consumers/proxy-auth.ts @@ -39,7 +39,7 @@ export class ProxyAuth extends ServiceMap.Service()("@hazel/auth/Prox const userLookupCache = yield* UserLookupCache const workos = yield* WorkOSClient const db = yield* Database.Database - const decodeClaims = Schema.decodeUnknown(WorkOSJwtClaims) + const decodeClaims = Schema.decodeUnknownEffect(WorkOSJwtClaims) const resolveInternalOrganizationId = ( workosOrgId: WorkOSOrganizationId, @@ -53,7 +53,7 @@ export class ProxyAuth extends ServiceMap.Service()("@hazel/auth/Prox workosOrgId, }).pipe(Effect.as(undefined)), onSome: (externalId) => - Schema.decodeUnknown(OrganizationId)(externalId).pipe( + Schema.decodeUnknownEffect(OrganizationId)(externalId).pipe( Effect.catch((error) => Effect.logWarning( "Failed to decode WorkOS external organization ID", @@ -106,13 +106,13 @@ export class ProxyAuth extends ServiceMap.Service()("@hazel/auth/Prox .limit(1), ) .pipe( - Effect.catchTag( - "DatabaseError", - (error) => + Effect.catchTag("DatabaseError", (error) => + Effect.fail( new ProxyAuthenticationError({ message: "Failed to lookup user in database", detail: error.message, }), + ), ), ) const userOption = Option.fromNullishOr(userResult[0]) @@ -155,7 +155,7 @@ export class ProxyAuth extends ServiceMap.Service()("@hazel/auth/Prox }) const { payload } = yield* verifyWithIssuer("https://api.workos.com").pipe( - Effect.orElse(() => verifyWithIssuer(`https://api.workos.com/user_management/${clientId}`)), + Effect.catch(() => verifyWithIssuer(`https://api.workos.com/user_management/${clientId}`)), ) const claims = yield* decodeClaims(payload).pipe( diff --git a/packages/auth/src/errors.ts b/packages/auth/src/errors.ts index b564d0622..1922f58bd 100644 --- a/packages/auth/src/errors.ts +++ b/packages/auth/src/errors.ts @@ -11,10 +11,13 @@ export class SessionCacheError extends Schema.TaggedErrorClass()("UserLookupCacheError", { - message: Schema.String, - cause: Schema.optional(Schema.Unknown), -}) {} +export class UserLookupCacheError extends Schema.TaggedErrorClass()( + "UserLookupCacheError", + { + message: Schema.String, + cause: Schema.optional(Schema.Unknown), + }, +) {} /** * Error thrown when fetching organization from WorkOS fails diff --git a/packages/auth/src/metrics.ts b/packages/auth/src/metrics.ts index 80ea629d0..12e18a1f5 100644 --- a/packages/auth/src/metrics.ts +++ b/packages/auth/src/metrics.ts @@ -3,7 +3,7 @@ * * Provides counters and histograms for monitoring auth performance. */ -import { Metric, MetricBoundaries } from "effect" +import { Metric } from "effect" // ============================================================================ // Counters @@ -20,13 +20,11 @@ export const userLookupCacheMisses = Metric.counter("user_lookup.cache.misses") // ============================================================================ /** User lookup cache operation latency (get/set) */ -export const userLookupCacheOperationLatency = Metric.histogram( - "user_lookup.cache.operation.latency_ms", - MetricBoundaries.fromIterable([1, 2, 5, 10, 25, 50]), -) +export const userLookupCacheOperationLatency = Metric.histogram("user_lookup.cache.operation.latency_ms", { + boundaries: [1, 2, 5, 10, 25, 50], +}) /** WorkOS organization lookup latency */ -export const orgLookupLatency = Metric.histogram( - "session.org.lookup.latency_ms", - MetricBoundaries.fromIterable([5, 10, 25, 50, 100, 250, 500]), -) +export const orgLookupLatency = Metric.histogram("session.org.lookup.latency_ms", { + boundaries: [5, 10, 25, 50, 100, 250, 500], +}) diff --git a/packages/auth/src/session/jwt-decoder.ts b/packages/auth/src/session/jwt-decoder.ts index 916101286..4e50310ad 100644 --- a/packages/auth/src/session/jwt-decoder.ts +++ b/packages/auth/src/session/jwt-decoder.ts @@ -9,7 +9,7 @@ export const decodeSessionJwt = (accessToken: string): Effect.Effect new InvalidJwtPayloadError({ diff --git a/packages/auth/src/session/workos-client.ts b/packages/auth/src/session/workos-client.ts index 1c5ecaddc..65541e75a 100644 --- a/packages/auth/src/session/workos-client.ts +++ b/packages/auth/src/session/workos-client.ts @@ -54,9 +54,7 @@ export class WorkOSClient extends ServiceMap.Service()("@hazel/aut } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(AuthConfig.layer), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(AuthConfig.layer)) /** Default mock user for tests */ static readonly mockUser: WorkOSUser = { @@ -90,7 +88,6 @@ export class WorkOSClient extends ServiceMap.Service()("@hazel/aut /** Test layer with mock WorkOS responses */ static Test = Layer.mock(this, { - _tag: "@hazel/auth/WorkOSClient", getUser: (userId: WorkOSUserId) => Effect.succeed({ ...WorkOSClient.mockUser, id: userId }), getOrganization: (orgId: WorkOSOrganizationId) => Effect.succeed({ ...WorkOSClient.mockOrganization, id: orgId }), @@ -100,7 +97,6 @@ export class WorkOSClient extends ServiceMap.Service()("@hazel/aut /** Test layer factory for configurable WorkOS behavior */ static TestWith = (options: { user?: WorkOSUser; organization?: Organization }) => Layer.mock(WorkOSClient, { - _tag: "@hazel/auth/WorkOSClient", getUser: (userId: WorkOSUserId) => Effect.succeed({ ...(options.user ?? WorkOSClient.mockUser), id: userId }), getOrganization: (orgId: WorkOSOrganizationId) => diff --git a/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts b/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts index 36492f16f..b04108f06 100644 --- a/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts @@ -2,7 +2,7 @@ import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@ import { ChatSyncChannelLink } from "@hazel/domain/models" import type { ChannelId, ExternalChannelId, SyncChannelLinkId, SyncConnectionId } from "@hazel/schema" -import { ServiceMap, Effect, Option, Schema } from "effect" +import { ServiceMap, Effect, Layer, Option, Schema } from "effect" export class ChatSyncChannelLinkRepo extends ServiceMap.Service()( "ChatSyncChannelLinkRepo", @@ -293,4 +293,6 @@ export class ChatSyncChannelLinkRepo extends ServiceMap.Service()( "ChatSyncConnectionRepo", @@ -181,4 +181,6 @@ export class ChatSyncConnectionRepo extends ServiceMap.Service()( "ChatSyncEventReceiptRepo", @@ -219,4 +219,6 @@ export class ChatSyncEventReceiptRepo extends ServiceMap.Service()( "ChatSyncMessageLinkRepo", @@ -213,4 +213,6 @@ export class ChatSyncMessageLinkRepo extends ServiceMap.Service()( "ConnectConversationChannelRepo", @@ -91,4 +91,6 @@ export class ConnectConversationChannelRepo extends ServiceMap.Service()( "ConnectConversationRepo", @@ -41,4 +41,6 @@ export class ConnectConversationRepo extends ServiceMap.Service()( "ConnectParticipantRepo", @@ -112,4 +112,6 @@ export class ConnectParticipantRepo extends ServiceMap.Service()( "GitHubSubscriptionRepo", @@ -133,4 +133,6 @@ export class GitHubSubscriptionRepo extends ServiceMap.Service()( "IntegrationConnectionRepo", @@ -323,7 +323,7 @@ export class IntegrationConnectionRepo extends ServiceMap.Service()("Invita execute((client) => client .insert(schema.invitationsTable) - .values(input) + .values(input as any) .onConflictDoUpdate({ target: schema.invitationsTable.workosInvitationId, set: { status: input.status, - acceptedAt: input.acceptedAt, + acceptedAt: input.acceptedAt as any, acceptedBy: input.acceptedBy, }, }) diff --git a/packages/backend-core/src/repositories/organization-member-repo.ts b/packages/backend-core/src/repositories/organization-member-repo.ts index dae97fe01..bf7dc2957 100644 --- a/packages/backend-core/src/repositories/organization-member-repo.ts +++ b/packages/backend-core/src/repositories/organization-member-repo.ts @@ -2,7 +2,7 @@ import { and, count, Database, eq, isNull, ModelRepository, schema, type TxFn } import type { OrganizationId, OrganizationMemberId, UserId } from "@hazel/schema" import { OrganizationMember } from "@hazel/domain/models" -import { ServiceMap, Effect, Option, type Schema } from "effect" +import { ServiceMap, Effect, Layer, Option, type Schema } from "effect" export class OrganizationMemberRepo extends ServiceMap.Service()( "OrganizationMemberRepo", @@ -50,7 +50,7 @@ export class OrganizationMemberRepo extends ServiceMap.Service return client .insert(schema.typingIndicatorsTable) .values({ - id: TypingIndicatorId.make(crypto.randomUUID()), + id: TypingIndicatorId.makeUnsafe(crypto.randomUUID()), channelId: params.channelId, memberId: params.memberId, lastTyped: params.lastTyped, diff --git a/packages/backend-core/src/repositories/user-presence-status-repo.ts b/packages/backend-core/src/repositories/user-presence-status-repo.ts index 88cc87041..edd2ceec3 100644 --- a/packages/backend-core/src/repositories/user-presence-status-repo.ts +++ b/packages/backend-core/src/repositories/user-presence-status-repo.ts @@ -2,12 +2,12 @@ import { and, Database, eq, inArray, lt, ModelRepository, ne, schema, type TxFn import type { ChannelId, UserId } from "@hazel/schema" import { UserPresenceStatus } from "@hazel/domain/models" -import { ServiceMap, Effect, Option, type Schema } from "effect" +import { ServiceMap, Effect, Layer, Option, type Schema } from "effect" export class UserPresenceStatusRepo extends ServiceMap.Service()( "UserPresenceStatusRepo", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const db = yield* Database.Database const baseRepo = yield* ModelRepository.makeRepository( schema.userPresenceStatusTable, @@ -39,14 +39,14 @@ export class UserPresenceStatusRepo extends ServiceMap.Service client .insert(schema.userPresenceStatusTable) - .values(input) + .values(input as any) .onConflictDoUpdate({ target: schema.userPresenceStatusTable.userId, set: { status: input.status, customMessage: input.customMessage, statusEmoji: input.statusEmoji, - statusExpiresAt: input.statusExpiresAt, + statusExpiresAt: input.statusExpiresAt as any, activeChannelId: input.activeChannelId, updatedAt: new Date(), lastSeenAt: new Date(), @@ -160,4 +160,6 @@ export class UserPresenceStatusRepo extends ServiceMap.Service()("UserRepo", { execute((client) => client .insert(schema.usersTable) - .values(input) + .values(input as any) .onConflictDoUpdate({ target: schema.usersTable.externalId, set: { diff --git a/packages/backend-core/src/services/workos-sync.test.ts b/packages/backend-core/src/services/workos-sync.test.ts index 8b677ebf9..eab87246c 100644 --- a/packages/backend-core/src/services/workos-sync.test.ts +++ b/packages/backend-core/src/services/workos-sync.test.ts @@ -32,7 +32,7 @@ describe("WorkOSSync helpers", () => { it("accepts org webhook payloads with missing externalId", async () => { const payload = await Effect.runPromise( - Schema.decodeUnknown(WorkOSSyncOrganizationPayload)({ + Schema.decodeUnknownEffect(WorkOSSyncOrganizationPayload)({ id: "org_01ABC123", name: "Acme", }), diff --git a/packages/backend-core/src/services/workos-sync.ts b/packages/backend-core/src/services/workos-sync.ts index ea687c10e..7242fc6d8 100644 --- a/packages/backend-core/src/services/workos-sync.ts +++ b/packages/backend-core/src/services/workos-sync.ts @@ -8,7 +8,7 @@ import { type UserId, } from "@hazel/schema" import type { Event } from "@workos-inc/node" -import { ServiceMap, Effect, Layer, Match, Option, pipe, Schema, Stream } from "effect" +import { ServiceMap, Effect, Layer, Match, Option, pipe, Result, Schema, Stream } from "effect" import { InvitationRepo } from "../repositories/invitation-repo" import { OrganizationMemberRepo } from "../repositories/organization-member-repo" import { OrganizationRepo } from "../repositories/organization-repo" @@ -76,12 +76,12 @@ export const WorkOSSyncMembershipRemovedPayload = Schema.Struct({ }) export const decodeInternalOrganizationId = (externalId: string) => - Schema.decodeUnknown(OrganizationId)(externalId) + Schema.decodeUnknownEffect(OrganizationId)(externalId) export const normalizeWorkOSRole = ( role: unknown, ): Effect.Effect, never> => - Schema.decodeUnknown(WorkOSRole)(role).pipe( + Schema.decodeUnknownEffect(WorkOSRole)(role).pipe( Effect.orElseSucceed((): Schema.Schema.Type => "member"), ) @@ -94,9 +94,9 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { const orgMemberRepo = yield* OrganizationMemberRepo const invitationRepo = yield* InvitationRepo const decodeWebhookData = - (schema: S) => + (schema: S) => (data: unknown) => - Schema.decodeUnknown(schema)(data).pipe( + Schema.decodeUnknownEffect(schema)(data).pipe( Effect.mapError( (error) => new WorkOSSyncError({ @@ -105,9 +105,9 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { }), ), ) - const decodeWorkOSUserId = Schema.decodeUnknown(WorkOSUserId) - const decodeWorkOSOrganizationId = Schema.decodeUnknown(WorkOSOrganizationId) - const decodeWorkOSInvitationId = Schema.decodeUnknown(WorkOSInvitationId) + const decodeWorkOSUserId = Schema.decodeUnknownEffect(WorkOSUserId) + const decodeWorkOSOrganizationId = Schema.decodeUnknownEffect(WorkOSOrganizationId) + const decodeWorkOSInvitationId = Schema.decodeUnknownEffect(WorkOSInvitationId) const resolveInternalOrganizationId = (externalId: string, workosOrgId: string) => decodeInternalOrganizationId(externalId).pipe( @@ -134,23 +134,23 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { ) => pipe( effect, - Effect.either, + Effect.result, // biome-ignore lint/suspicious/useIterableCallbackReturn: - Effect.map((either) => { - if (either._tag === "Right") { - onSuccess(either.right) + Effect.map((r) => { + if (Result.isSuccess(r)) { + onSuccess(r.success) } else { - onError(either.left) + onError(r.failure) } }), ) // Pagination helpers using Effect Stream to fetch all pages from WorkOS - // Stream.paginateEffect takes initial cursor and returns Effect<[pageData, Option]> + // Stream.paginate takes initial cursor and returns Effect<[pageData, Option]> // Stream.runCollect gathers all pages, then we flatten them into a single array const fetchAllUsers = pipe( - Stream.paginateEffect(undefined as string | undefined, (after) => + Stream.paginate(undefined as string | undefined, (after) => workos .call((client) => client.userManagement.listUsers({ limit: 100, after })) .pipe( @@ -166,7 +166,7 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { ) const fetchAllOrganizations = pipe( - Stream.paginateEffect(undefined as string | undefined, (after) => + Stream.paginate(undefined as string | undefined, (after) => workos .call((client) => client.organizations.listOrganizations({ limit: 100, after })) .pipe( @@ -183,7 +183,7 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { const fetchAllMemberships = (workosOrgId: WorkOSOrganizationId) => pipe( - Stream.paginateEffect(undefined as string | undefined, (after) => + Stream.paginate(undefined as string | undefined, (after) => workos .call((client) => client.userManagement.listOrganizationMemberships({ @@ -211,7 +211,7 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { const fetchAllInvitations = (workosOrgId: WorkOSOrganizationId) => pipe( - Stream.paginateEffect(undefined as string | undefined, (after) => + Stream.paginate(undefined as string | undefined, (after) => workos .call((client) => client.userManagement.listInvitations({ @@ -244,14 +244,14 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { // Fetch all users from WorkOS (with pagination) yield* Effect.logInfo("Fetching users from WorkOS...") const fetchStart = Date.now() - const workosUsersResult = yield* pipe(fetchAllUsers, Effect.either) + const workosUsersResult = yield* pipe(fetchAllUsers, Effect.result) - if (workosUsersResult._tag === "Left") { - result.errors.push(`Failed to fetch users from WorkOS: ${workosUsersResult.left}`) + if (workosUsersResult._tag === "Failure") { + result.errors.push(`Failed to fetch users from WorkOS: ${workosUsersResult.failure}`) return result } - const workosUsers = workosUsersResult.right + const workosUsers = workosUsersResult.success yield* Effect.logInfo( `Fetched ${workosUsers.length} users from WorkOS in ${Date.now() - fetchStart}ms`, ) @@ -343,14 +343,14 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { // Fetch all organizations from WorkOS (with pagination) yield* Effect.logInfo("Fetching organizations from WorkOS...") const fetchStart = Date.now() - const workosOrgsResult = yield* pipe(fetchAllOrganizations, Effect.either) + const workosOrgsResult = yield* pipe(fetchAllOrganizations, Effect.result) - if (workosOrgsResult._tag === "Left") { - result.errors.push(`Failed to fetch organizations from WorkOS: ${workosOrgsResult.left}`) + if (workosOrgsResult._tag === "Failure") { + result.errors.push(`Failed to fetch organizations from WorkOS: ${workosOrgsResult.failure}`) return result } - const workosOrgs = workosOrgsResult.right + const workosOrgs = workosOrgsResult.success yield* Effect.logInfo( `Fetched ${workosOrgs.length} organizations from WorkOS in ${Date.now() - fetchStart}ms`, ) @@ -438,43 +438,43 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { // Fetch WorkOS organization by our internal organization ID (stored as externalId in WorkOS) const workosOrgResult = yield* pipe( workos.call((client) => client.organizations.getOrganizationByExternalId(organizationId)), - Effect.either, + Effect.result, ) - if (workosOrgResult._tag === "Left") { + if (workosOrgResult._tag === "Failure") { result.errors.push( - `Failed to fetch WorkOS org for ${organizationId}: ${workosOrgResult.left}`, + `Failed to fetch WorkOS org for ${organizationId}: ${workosOrgResult.failure}`, ) return result } - const workosOrg = workosOrgResult.right + const workosOrg = workosOrgResult.success // Fetch memberships from WorkOS using WorkOS organization ID (with pagination) const workosOrganizationId = yield* decodeWorkOSOrganizationId(workosOrg.id).pipe( Effect.mapError((error) => String(String(error))), - Effect.either, + Effect.result, ) - if (workosOrganizationId._tag === "Left") { + if (workosOrganizationId._tag === "Failure") { result.errors.push( - `Failed to decode WorkOS organization ID for ${organizationId}: ${workosOrganizationId.left}`, + `Failed to decode WorkOS organization ID for ${organizationId}: ${workosOrganizationId.failure}`, ) return result } const workosMembershipsResult = yield* pipe( - fetchAllMemberships(workosOrganizationId.right), - Effect.either, + fetchAllMemberships(workosOrganizationId.success), + Effect.result, ) - if (workosMembershipsResult._tag === "Left") { + if (workosMembershipsResult._tag === "Failure") { result.errors.push( - `Failed to fetch memberships for org ${organizationId}: ${workosMembershipsResult.left}`, + `Failed to fetch memberships for org ${organizationId}: ${workosMembershipsResult.failure}`, ) return result } - const workosMemberships = workosMembershipsResult.right + const workosMemberships = workosMembershipsResult.success // Get all existing memberships for this org const existingMemberships = yield* orgMemberRepo.findAllByOrganization(organizationId) @@ -565,43 +565,43 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { // Fetch WorkOS organization by our internal organization ID (stored as externalId in WorkOS) const workosOrgResult = yield* pipe( workos.call((client) => client.organizations.getOrganizationByExternalId(organizationId)), - Effect.either, + Effect.result, ) - if (workosOrgResult._tag === "Left") { + if (workosOrgResult._tag === "Failure") { result.errors.push( - `Failed to fetch WorkOS org for ${organizationId}: ${workosOrgResult.left}`, + `Failed to fetch WorkOS org for ${organizationId}: ${workosOrgResult.failure}`, ) return result } - const workosOrg = workosOrgResult.right + const workosOrg = workosOrgResult.success // Fetch invitations from WorkOS using WorkOS organization ID (with pagination) const workosOrganizationId = yield* decodeWorkOSOrganizationId(workosOrg.id).pipe( Effect.mapError((error) => String(String(error))), - Effect.either, + Effect.result, ) - if (workosOrganizationId._tag === "Left") { + if (workosOrganizationId._tag === "Failure") { result.errors.push( - `Failed to decode WorkOS organization ID for ${organizationId}: ${workosOrganizationId.left}`, + `Failed to decode WorkOS organization ID for ${organizationId}: ${workosOrganizationId.failure}`, ) return result } const workosInvitationsResult = yield* pipe( - fetchAllInvitations(workosOrganizationId.right), - Effect.either, + fetchAllInvitations(workosOrganizationId.success), + Effect.result, ) - if (workosInvitationsResult._tag === "Left") { + if (workosInvitationsResult._tag === "Failure") { result.errors.push( - `Failed to fetch invitations for org ${organizationId}: ${workosInvitationsResult.left}`, + `Failed to fetch invitations for org ${organizationId}: ${workosInvitationsResult.failure}`, ) return result } - const workosInvitations = workosInvitationsResult.right + const workosInvitations = workosInvitationsResult.success // Get all existing invitations for this org const existingInvitations = yield* invitationRepo.findAllByOrganization(organizationId) @@ -673,12 +673,12 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { ) // Mark expired invitations - const expiredResult = yield* pipe(invitationRepo.markExpired(), Effect.either) + const expiredResult = yield* pipe(invitationRepo.markExpired(), Effect.result) - if (expiredResult._tag === "Right") { - result.expired = expiredResult.right.length + if (expiredResult._tag === "Success") { + result.expired = expiredResult.success.length } else { - result.errors.push(`Error marking expired invitations: ${expiredResult.left}`) + result.errors.push(`Error marking expired invitations: ${expiredResult.failure}`) } return result @@ -856,15 +856,15 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { // Fetch WorkOS org to get externalId (our internal org ID) const workosOrgResult = yield* pipe( workos.call((client) => client.organizations.getOrganization(data.organizationId)), - Effect.either, + Effect.result, ) - if (workosOrgResult._tag === "Left") { + if (workosOrgResult._tag === "Failure") { yield* Effect.logError(`Failed to fetch WorkOS org ${data.organizationId}`) return } - const workosOrg = workosOrgResult.right + const workosOrg = workosOrgResult.success if (!workosOrg.externalId) { yield* Effect.logWarning(`WorkOS org ${data.organizationId} has no externalId`) @@ -901,15 +901,15 @@ export class WorkOSSync extends ServiceMap.Service()("WorkOSSync", { // Fetch WorkOS org to get externalId (our internal org ID) const workosOrgResult = yield* pipe( workos.call((client) => client.organizations.getOrganization(data.organizationId)), - Effect.either, + Effect.result, ) - if (workosOrgResult._tag === "Left") { + if (workosOrgResult._tag === "Failure") { yield* Effect.logError(`Failed to fetch WorkOS org ${data.organizationId}`) return } - const workosOrg = workosOrgResult.right + const workosOrg = workosOrgResult.success if (!workosOrg.externalId) { yield* Effect.logWarning(`WorkOS org ${data.organizationId} has no externalId`) diff --git a/packages/db/src/services/database.test.ts b/packages/db/src/services/database.test.ts index 05b8ac33a..1114a5b4a 100644 --- a/packages/db/src/services/database.test.ts +++ b/packages/db/src/services/database.test.ts @@ -41,7 +41,7 @@ describe("Database.transaction", () => { const txCtx = yield* TransactionContext // Step 1: Insert a record using the transaction client - yield* txCtx.execute((tx) => + yield* txCtx.execute((tx: any) => tx.execute(`INSERT INTO _test_rollback (id) VALUES ('${testId}')`), ) @@ -49,14 +49,15 @@ describe("Database.transaction", () => { return yield* Effect.fail(new Error("Intentional failure")) }), ) - .pipe(Effect.either) + .pipe(Effect.result) // Verify the transaction failed - expect(result._tag).toBe("Left") + expect(result._tag).toBe("Failure") // Verify the insert was rolled back (query outside transaction) const rows = yield* db.execute( - (client) => client.$client`SELECT * FROM _test_rollback WHERE id = ${testId}`, + (client: any) => + client.$client`SELECT * FROM _test_rollback WHERE id = ${testId}` as Promise, ) // Should be empty because transaction rolled back @@ -80,7 +81,7 @@ describe("Database.transaction", () => { // Get the transaction context to execute within the transaction const txCtx = yield* TransactionContext - yield* txCtx.execute((tx) => + yield* txCtx.execute((tx: any) => tx.execute(`INSERT INTO _test_rollback (id) VALUES ('${testId}')`), ) return "success" @@ -89,13 +90,16 @@ describe("Database.transaction", () => { // Verify the insert persisted (query outside transaction) const rows = yield* db.execute( - (client) => client.$client`SELECT * FROM _test_rollback WHERE id = ${testId}`, + (client: any) => + client.$client`SELECT * FROM _test_rollback WHERE id = ${testId}` as Promise, ) expect(rows.length).toBe(1) // Cleanup - yield* db.execute((client) => client.$client`DELETE FROM _test_rollback WHERE id = ${testId}`) + yield* db.execute( + (client: any) => client.$client`DELETE FROM _test_rollback WHERE id = ${testId}`, + ) }) await Effect.runPromise( @@ -120,25 +124,26 @@ describe("Database.transaction", () => { // Get the transaction context to execute within the transaction const txCtx = yield* TransactionContext - yield* txCtx.execute((tx) => + yield* txCtx.execute((tx: any) => tx.execute(`INSERT INTO _test_rollback (id) VALUES ('${testId}')`), ) return yield* Effect.fail(new CustomTestError("Custom error message")) }), ) - .pipe(Effect.either) + .pipe(Effect.result) // Verify the transaction failed with the correct error type - expect(result._tag).toBe("Left") - if (result._tag === "Left") { - const cause = result.left + expect(result._tag).toBe("Failure") + if (result._tag === "Failure") { + const failure = result.failure // The error should be our custom error - expect(cause).toBeDefined() + expect(failure).toBeDefined() } // Verify rollback occurred (query outside transaction) const rows = yield* db.execute( - (client) => client.$client`SELECT * FROM _test_rollback WHERE id = ${testId}`, + (client: any) => + client.$client`SELECT * FROM _test_rollback WHERE id = ${testId}` as Promise, ) expect(rows.length).toBe(0) }) diff --git a/packages/db/src/services/database.ts b/packages/db/src/services/database.ts index 89841508f..bb3ad95c4 100644 --- a/packages/db/src/services/database.ts +++ b/packages/db/src/services/database.ts @@ -29,7 +29,9 @@ export interface TransactionService { readonly execute: TxFn } -export class TransactionContext extends ServiceMap.Service()("TransactionContext") {} +export class TransactionContext extends ServiceMap.Service()( + "TransactionContext", +) {} const DatabaseErrorType = Schema.Literals([ "unique_violation", @@ -103,7 +105,7 @@ const makeService = (config: Config) => yield* Effect.tryPromise(() => sql`SELECT 1`).pipe( Effect.retry( - Schedule.jittered(Schedule.spaced("1.25 seconds"), { min: 0.5, max: 1.5 }).pipe( + Schedule.jittered(Schedule.spaced("1.25 seconds")).pipe( Schedule.both(Schedule.recurs(10)), Schedule.tapOutput(([output]) => Effect.logWarning( @@ -176,7 +178,7 @@ const makeService = (config: Config) => execute: ( fn: (client: Client | TransactionClient) => Promise, ) => Effect.Effect, - validatedInput: Schema.Schema.Type, + validatedInput: InputSchema["Type"], options?: { spanPrefix?: string }, ) => Effect.Effect, ) => { @@ -203,7 +205,7 @@ const makeService = (config: Config) => Effect.withSpan("queryWithSchema", { attributes: { "input.schema": inputSchema.ast.toString() }, }), - ) + ) as Effect.Effect } } diff --git a/packages/db/src/services/drizzle-effect.ts b/packages/db/src/services/drizzle-effect.ts index 2147f92d2..270be3c1f 100644 --- a/packages/db/src/services/drizzle-effect.ts +++ b/packages/db/src/services/drizzle-effect.ts @@ -66,16 +66,15 @@ export const JsonValue = Schema.Union([ ]) satisfies Schema.Schema // For cases where you need full JSON validation, use this explicit version -export const StrictJsonValue: Schema.Schema = Schema.suspend( - () => - Schema.Union([ - Schema.String, - Schema.Number, - Schema.Boolean, - Schema.Null, - Schema.Record(Schema.String, StrictJsonValue), - Schema.Array(StrictJsonValue), - ]), +export const StrictJsonValue: Schema.Schema = Schema.suspend(() => + Schema.Union([ + Schema.String, + Schema.Number, + Schema.Boolean, + Schema.Null, + Schema.Record(Schema.String, StrictJsonValue), + Schema.Array(StrictJsonValue), + ]), ) // Utility type to prevent unknown keys (similar to drizzle-zod) @@ -157,13 +156,8 @@ export function createInsertSchema [ name, - typeof refineColumn === "function" && - !Schema.isSchema(refineColumn) - ? ( - refineColumn as ( - schema: Schema.Top, - ) => Schema.Top - )(schemaEntries[name]!) + typeof refineColumn === "function" && !Schema.isSchema(refineColumn) + ? (refineColumn as (schema: Schema.Top) => Schema.Top)(schemaEntries[name]!) : refineColumn, ]) @@ -204,13 +198,8 @@ export function createSelectSchema [ name, - typeof refineColumn === "function" && - !Schema.isSchema(refineColumn) - ? ( - refineColumn as ( - schema: Schema.Top, - ) => Schema.Top - )(schemaEntries[name]!) + typeof refineColumn === "function" && !Schema.isSchema(refineColumn) + ? (refineColumn as (schema: Schema.Top) => Schema.Top)(schemaEntries[name]!) : refineColumn, ]) diff --git a/packages/db/src/services/model-repository.ts b/packages/db/src/services/model-repository.ts index 33cdaab92..d0e74b555 100644 --- a/packages/db/src/services/model-repository.ts +++ b/packages/db/src/services/model-repository.ts @@ -25,20 +25,20 @@ export function makeRepository< const insert = (data: S["insert"]["Type"], tx?: TxFn) => pipe( - db.makeQueryWithSchema(schema.insert as Schema.Schema, (execute, input) => + db.makeQueryWithSchema(schema.insert as Schema.Top, (execute, input: any) => execute((client) => client.insert(table).values([input]).returning()), )(data, tx), ) as unknown as Effect.Effect const insertVoid = (data: S["insert"]["Type"], tx?: TxFn) => - db.makeQueryWithSchema(schema.insert as Schema.Schema, (execute, input) => + db.makeQueryWithSchema(schema.insert as Schema.Top, (execute, input: any) => execute((client) => client.insert(table).values(input)), )(data, tx) as unknown as Effect.Effect const update = (data: S["update"]["Type"], tx?: TxFn) => db.makeQueryWithSchema( - (schema.update as Schema.Struct).mapFields(Struct.map(Schema.optional)), - (execute, input) => + (schema.update as Schema.Struct).mapFields(Struct.map(Schema.optional)) as Schema.Top, + (execute, input: any) => execute((client) => client .update(table) @@ -57,8 +57,8 @@ export function makeRepository< const updateVoid = (data: S["update"]["Type"], tx?: TxFn) => db.makeQueryWithSchema( - (schema.update as Schema.Struct).mapFields(Struct.map(Schema.optional)), - (execute, input) => + (schema.update as Schema.Struct).mapFields(Struct.map(Schema.optional)) as Schema.Top, + (execute, input: any) => execute((client) => client .update(table) diff --git a/packages/db/src/services/model.ts b/packages/db/src/services/model.ts index a3e31047c..b7ae163ec 100644 --- a/packages/db/src/services/model.ts +++ b/packages/db/src/services/model.ts @@ -23,7 +23,13 @@ export class EntityNotFound extends Schema.TaggedErrorClass()("E id: Schema.Any, }) {} -export interface Repository { +export interface Repository< + RecordType, + S extends EntitySchema, + Col extends string & keyof S["update"]["Type"], + Name extends string, + Id, +> { readonly insert: ( insert: S["insert"]["Type"], tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, diff --git a/packages/domain/src/cluster/activities/message-activities.ts b/packages/domain/src/cluster/activities/message-activities.ts index a29b9045c..110ac8afe 100644 --- a/packages/domain/src/cluster/activities/message-activities.ts +++ b/packages/domain/src/cluster/activities/message-activities.ts @@ -73,4 +73,7 @@ export class CreateNotificationError extends Schema.TaggedErrorClass( } /** AI response could not be parsed or was empty */ -export class AIResponseParseError extends Schema.TaggedErrorClass()("AIResponseParseError", { - threadChannelId: ChannelId, - rawResponse: Schema.String.pipe(Schema.optional), -}) { +export class AIResponseParseError extends Schema.TaggedErrorClass()( + "AIResponseParseError", + { + threadChannelId: ChannelId, + rawResponse: Schema.String.pipe(Schema.optional), + }, +) { readonly retryable = false // Bad data won't fix itself } diff --git a/packages/domain/src/current-user.ts b/packages/domain/src/current-user.ts index df2331dd4..6159a880e 100644 --- a/packages/domain/src/current-user.ts +++ b/packages/domain/src/current-user.ts @@ -41,9 +41,12 @@ const AuthFailure = S.Union([ WorkOSUserFetchError, ]) -export class Authorization extends HttpApiMiddleware.Service()("Authorization", { +export class Authorization extends HttpApiMiddleware.Service< + Authorization, + { + provides: Context + } +>()("Authorization", { error: AuthFailure, security: { bearer: HttpApiSecurity.bearer, diff --git a/packages/domain/src/desktop-auth-errors.ts b/packages/domain/src/desktop-auth-errors.ts index 4fb97dbd6..2c92f94b9 100644 --- a/packages/domain/src/desktop-auth-errors.ts +++ b/packages/domain/src/desktop-auth-errors.ts @@ -52,9 +52,12 @@ export class OAuthCallbackError extends Schema.TaggedErrorClass()("MissingAuthCodeError", { - message: Schema.String, -}) {} +export class MissingAuthCodeError extends Schema.TaggedErrorClass()( + "MissingAuthCodeError", + { + message: Schema.String, + }, +) {} // ============================================================================ // Token Storage Errors diff --git a/packages/domain/src/errors.ts b/packages/domain/src/errors.ts index 3d1bb2363..87932e552 100644 --- a/packages/domain/src/errors.ts +++ b/packages/domain/src/errors.ts @@ -18,7 +18,9 @@ export class UnauthorizedError extends Schema.TaggedErrorClass("OAuthCodeExpiredError")( +export class OAuthCodeExpiredError extends Schema.TaggedErrorClass( + "OAuthCodeExpiredError", +)( "OAuthCodeExpiredError", { message: Schema.String, @@ -66,7 +68,9 @@ export class DmChannelAlreadyExistsError extends Schema.TaggedErrorClass("MessageNotFoundError")( +export class MessageNotFoundError extends Schema.TaggedErrorClass( + "MessageNotFoundError", +)( "MessageNotFoundError", { messageId: MessageId, diff --git a/packages/domain/src/http/bot-commands.ts b/packages/domain/src/http/bot-commands.ts index 1d61853c8..994259fe8 100644 --- a/packages/domain/src/http/bot-commands.ts +++ b/packages/domain/src/http/bot-commands.ts @@ -69,10 +69,13 @@ export class BotNotFoundError extends Schema.TaggedErrorClass( botId: BotId, }) {} -export class BotNotInstalledError extends Schema.TaggedErrorClass()("BotNotInstalledError", { - botId: BotId, - orgId: OrganizationId, -}) {} +export class BotNotInstalledError extends Schema.TaggedErrorClass()( + "BotNotInstalledError", + { + botId: BotId, + orgId: OrganizationId, + }, +) {} export class BotCommandNotFoundError extends Schema.TaggedErrorClass()( "BotCommandNotFoundError", diff --git a/packages/domain/src/http/chat-sync.ts b/packages/domain/src/http/chat-sync.ts index 77551e548..008e77187 100644 --- a/packages/domain/src/http/chat-sync.ts +++ b/packages/domain/src/http/chat-sync.ts @@ -111,7 +111,12 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") params: { orgId: OrganizationId }, payload: CreateChatSyncConnectionRequest, success: ChatSyncConnectionResponse, - error: [ChatSyncConnectionExistsError, ChatSyncIntegrationNotConnectedError, UnauthorizedError, InternalServerError], + error: [ + ChatSyncConnectionExistsError, + ChatSyncIntegrationNotConnectedError, + UnauthorizedError, + InternalServerError, + ], }) .annotateMerge( OpenApi.annotations({ @@ -157,7 +162,12 @@ export class ChatSyncGroup extends HttpApiGroup.make("chat-sync") params: { syncConnectionId: SyncConnectionId }, payload: CreateChatSyncChannelLinkRequest, success: ChatSyncChannelLinkResponse, - error: [ChatSyncConnectionNotFoundError, ChatSyncChannelLinkExistsError, UnauthorizedError, InternalServerError], + error: [ + ChatSyncConnectionNotFoundError, + ChatSyncChannelLinkExistsError, + UnauthorizedError, + InternalServerError, + ], }) .annotateMerge( OpenApi.annotations({ diff --git a/packages/domain/src/http/incoming-webhooks.ts b/packages/domain/src/http/incoming-webhooks.ts index 95e801dbc..fae136f05 100644 --- a/packages/domain/src/http/incoming-webhooks.ts +++ b/packages/domain/src/http/incoming-webhooks.ts @@ -136,7 +136,12 @@ export class IncomingWebhookGroup extends HttpApiGroup.make("incoming-webhooks") }, payload: IncomingWebhookPayload, success: WebhookMessageResponse, - error: [WebhookNotFoundError, WebhookDisabledError, InvalidWebhookTokenError, InternalServerError], + error: [ + WebhookNotFoundError, + WebhookDisabledError, + InvalidWebhookTokenError, + InternalServerError, + ], }) .annotateMerge( OpenApi.annotations({ @@ -156,7 +161,12 @@ export class IncomingWebhookGroup extends HttpApiGroup.make("incoming-webhooks") }, payload: OpenStatusPayload, success: WebhookMessageResponse, - error: [WebhookNotFoundError, WebhookDisabledError, InvalidWebhookTokenError, InternalServerError], + error: [ + WebhookNotFoundError, + WebhookDisabledError, + InvalidWebhookTokenError, + InternalServerError, + ], }) .annotateMerge( OpenApi.annotations({ @@ -176,7 +186,12 @@ export class IncomingWebhookGroup extends HttpApiGroup.make("incoming-webhooks") }, payload: RailwayPayload, success: WebhookMessageResponse, - error: [WebhookNotFoundError, WebhookDisabledError, InvalidWebhookTokenError, InternalServerError], + error: [ + WebhookNotFoundError, + WebhookDisabledError, + InvalidWebhookTokenError, + InternalServerError, + ], }) .annotateMerge( OpenApi.annotations({ diff --git a/packages/domain/src/http/integration-resources.ts b/packages/domain/src/http/integration-resources.ts index 09fae9f23..36770844c 100644 --- a/packages/domain/src/http/integration-resources.ts +++ b/packages/domain/src/http/integration-resources.ts @@ -164,7 +164,13 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res params: { orgId: OrganizationId }, query: { url: Schema.String }, success: LinearIssueResourceResponse, - error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, ResourceNotFoundError, UnauthorizedError, InternalServerError], + error: [ + IntegrationNotConnectedForPreviewError, + IntegrationResourceError, + ResourceNotFoundError, + UnauthorizedError, + InternalServerError, + ], }) .annotateMerge( OpenApi.annotations({ @@ -180,7 +186,13 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res params: { orgId: OrganizationId }, query: { url: Schema.String }, success: GitHubPRResourceResponse, - error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, ResourceNotFoundError, UnauthorizedError, InternalServerError], + error: [ + IntegrationNotConnectedForPreviewError, + IntegrationResourceError, + ResourceNotFoundError, + UnauthorizedError, + InternalServerError, + ], }) .annotateMerge( OpenApi.annotations({ @@ -196,7 +208,9 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res params: { orgId: OrganizationId }, query: { page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => "1")), - perPage: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => "30")), + perPage: Schema.optional(Schema.NumberFromString).pipe( + Schema.withDecodingDefault(() => "30"), + ), }, success: GitHubRepositoriesResponse, error: [IntegrationNotConnectedForPreviewError, UnauthorizedError, InternalServerError], @@ -214,7 +228,12 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res HttpApiEndpoint.get("getDiscordGuilds", `/:orgId/discord/guilds`, { params: { orgId: OrganizationId }, success: DiscordGuildsResponse, - error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, UnauthorizedError, InternalServerError], + error: [ + IntegrationNotConnectedForPreviewError, + IntegrationResourceError, + UnauthorizedError, + InternalServerError, + ], }) .annotateMerge( OpenApi.annotations({ @@ -232,7 +251,12 @@ export class IntegrationResourceGroup extends HttpApiGroup.make("integration-res guildId: Schema.String, }, success: DiscordGuildChannelsResponse, - error: [IntegrationNotConnectedForPreviewError, IntegrationResourceError, UnauthorizedError, InternalServerError], + error: [ + IntegrationNotConnectedForPreviewError, + IntegrationResourceError, + UnauthorizedError, + InternalServerError, + ], }) .annotateMerge( OpenApi.annotations({ diff --git a/packages/domain/src/http/integrations.ts b/packages/domain/src/http/integrations.ts index f0219644c..096cbd626 100644 --- a/packages/domain/src/http/integrations.ts +++ b/packages/domain/src/http/integrations.ts @@ -173,7 +173,12 @@ export class IntegrationGroup extends HttpApiGroup.make("integrations") level: Schema.optional(ConnectionLevel), }, success: Schema.Void, - error: [IntegrationNotConnectedError, UnsupportedProviderError, UnauthorizedError, InternalServerError], + error: [ + IntegrationNotConnectedError, + UnsupportedProviderError, + UnauthorizedError, + InternalServerError, + ], }) .annotateMerge( OpenApi.annotations({ diff --git a/packages/domain/src/http/klipy.ts b/packages/domain/src/http/klipy.ts index 5c81812ac..4491d7f89 100644 --- a/packages/domain/src/http/klipy.ts +++ b/packages/domain/src/http/klipy.ts @@ -72,31 +72,32 @@ export class KlipyGroup extends HttpApiGroup.make("klipy") HttpApiEndpoint.get("trending", "/trending", { query: { page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => "1")), - per_page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => "25")), + per_page: Schema.optional(Schema.NumberFromString).pipe( + Schema.withDecodingDefault(() => "25"), + ), }, success: KlipySearchResponse, error: KlipyApiError, - }) - .annotate(RequiredScopes, ["messages:read"]), + }).annotate(RequiredScopes, ["messages:read"]), ) .add( HttpApiEndpoint.get("search", "/search", { query: { q: Schema.String, page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => "1")), - per_page: Schema.optional(Schema.NumberFromString).pipe(Schema.withDecodingDefault(() => "25")), + per_page: Schema.optional(Schema.NumberFromString).pipe( + Schema.withDecodingDefault(() => "25"), + ), }, success: KlipySearchResponse, error: KlipyApiError, - }) - .annotate(RequiredScopes, ["messages:read"]), + }).annotate(RequiredScopes, ["messages:read"]), ) .add( HttpApiEndpoint.get("categories", "/categories", { success: KlipyCategoriesResponse, error: KlipyApiError, - }) - .annotate(RequiredScopes, ["messages:read"]), + }).annotate(RequiredScopes, ["messages:read"]), ) .prefix("/klipy") .middleware(CurrentUser.Authorization) {} diff --git a/packages/domain/src/http/uploads.ts b/packages/domain/src/http/uploads.ts index f6eda3cc4..00dea1a5e 100644 --- a/packages/domain/src/http/uploads.ts +++ b/packages/domain/src/http/uploads.ts @@ -35,14 +35,14 @@ const BaseUploadFields = { fileSize: Schema.Number, } -const allowedAvatarTypeFilter = Schema.makeFilter( - (s) => ALLOWED_AVATAR_TYPES.includes(s as (typeof ALLOWED_AVATAR_TYPES)[number]) +const allowedAvatarTypeFilter = Schema.makeFilter((s) => + ALLOWED_AVATAR_TYPES.includes(s as (typeof ALLOWED_AVATAR_TYPES)[number]) ? undefined : "Content type must be image/jpeg, image/png, or image/webp", ) -const allowedEmojiTypeFilter = Schema.makeFilter( - (s) => ALLOWED_EMOJI_TYPES.includes(s as (typeof ALLOWED_EMOJI_TYPES)[number]) +const allowedEmojiTypeFilter = Schema.makeFilter((s) => + ALLOWED_EMOJI_TYPES.includes(s as (typeof ALLOWED_EMOJI_TYPES)[number]) ? undefined : "Content type must be image/png, image/gif, or image/webp", ) @@ -55,9 +55,12 @@ export class UserAvatarUploadRequest extends Schema.Class botId: BotId, contentType: Schema.String.check(allowedAvatarTypeFilter), fileSize: Schema.Number.check( - Schema.isBetween({ minimum: 1, maximum: MAX_AVATAR_SIZE }, { - message: "File size must be between 1 byte and 5MB", - }), + Schema.isBetween( + { minimum: 1, maximum: MAX_AVATAR_SIZE }, + { + message: "File size must be between 1 byte and 5MB", + }, + ), ), }) {} @@ -86,9 +92,12 @@ export class OrganizationAvatarUploadRequest extends Schema.Class("Bot")({ webhookUrl: Schema.NullOr(Schema.String), apiTokenHash: Schema.String, scopes: Schema.NullOr(Schema.Array(Schema.String)), - metadata: Schema.NullOr( - Schema.Record(Schema.String, Schema.Unknown), - ), + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), isPublic: Schema.Boolean, installCount: Schema.Number, // List of integration providers this bot is allowed to use (e.g., ["linear", "github"]) diff --git a/packages/domain/src/models/integration-connection-model.ts b/packages/domain/src/models/integration-connection-model.ts index 181141a01..b44a67842 100644 --- a/packages/domain/src/models/integration-connection-model.ts +++ b/packages/domain/src/models/integration-connection-model.ts @@ -3,7 +3,14 @@ import { Schema } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const IntegrationProvider = Schema.Literals(["linear", "github", "figma", "notion", "discord", "craft"]) +export const IntegrationProvider = Schema.Literals([ + "linear", + "github", + "figma", + "notion", + "discord", + "craft", +]) export type IntegrationProvider = Schema.Schema.Type export const ConnectionLevel = Schema.Literals(["organization", "user"]) diff --git a/packages/domain/src/models/message-embed-schema.ts b/packages/domain/src/models/message-embed-schema.ts index 36e9f0a2a..60eda0fca 100644 --- a/packages/domain/src/models/message-embed-schema.ts +++ b/packages/domain/src/models/message-embed-schema.ts @@ -50,7 +50,9 @@ export type MessageEmbedField = Schema.Schema.Type // Embed badge (for status indicators) export const MessageEmbedBadge = Schema.Struct({ text: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(64)), - color: Schema.optional(Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 16777215 }))), // 0x000000 to 0xFFFFFF + color: Schema.optional( + Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 16777215 })), + ), // 0x000000 to 0xFFFFFF }) export type MessageEmbedBadge = Schema.Schema.Type @@ -107,7 +109,9 @@ export const MessageEmbed = Schema.Struct({ title: Schema.optional(Schema.String.check(Schema.isMaxLength(256))), description: Schema.optional(Schema.String.check(Schema.isMaxLength(4096))), url: Schema.optional(Schema.String.check(Schema.isMaxLength(2048))), - color: Schema.optional(Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 16777215 }))), // 0x000000 to 0xFFFFFF + color: Schema.optional( + Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 16777215 })), + ), // 0x000000 to 0xFFFFFF author: Schema.optional(MessageEmbedAuthor), footer: Schema.optional(MessageEmbedFooter), image: Schema.optional(Schema.Struct({ url: Schema.String.check(Schema.isMaxLength(2048)) })), diff --git a/packages/domain/src/models/organization-model.ts b/packages/domain/src/models/organization-model.ts index 76bf8fd54..18e3a6c9a 100644 --- a/packages/domain/src/models/organization-model.ts +++ b/packages/domain/src/models/organization-model.ts @@ -8,9 +8,7 @@ export class Model extends M.Class("Organization")({ name: Schema.String, slug: Schema.NullOr(Schema.String), logoUrl: Schema.NullOr(Schema.String), - settings: Schema.NullOr( - Schema.Record(Schema.String, Schema.Unknown), - ), + settings: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), isPublic: Schema.Boolean, ...baseFields, }) {} diff --git a/packages/domain/src/models/user-model.ts b/packages/domain/src/models/user-model.ts index d35bfee2a..81c2dfb6a 100644 --- a/packages/domain/src/models/user-model.ts +++ b/packages/domain/src/models/user-model.ts @@ -10,9 +10,9 @@ export type UserType = Schema.Schema.Type /** * Time in HH:MM format (00:00 - 23:59) */ -export const TimeString = Schema.String.check( - Schema.isPattern(/^([01]\d|2[0-3]):([0-5]\d)$/), -).pipe(Schema.brand("TimeString")) +export const TimeString = Schema.String.check(Schema.isPattern(/^([01]\d|2[0-3]):([0-5]\d)$/)).pipe( + Schema.brand("TimeString"), +) export type TimeString = Schema.Schema.Type /** diff --git a/packages/domain/src/models/utils.ts b/packages/domain/src/models/utils.ts index 242144f40..5ffd92234 100644 --- a/packages/domain/src/models/utils.ts +++ b/packages/domain/src/models/utils.ts @@ -7,11 +7,10 @@ import * as Schema from "effect/Schema" import * as SchemaGetter from "effect/SchemaGetter" import * as SchemaIssue from "effect/SchemaIssue" -const { Class, Field, FieldExcept, FieldOnly, Struct, Union, extract, fieldEvolve } = - VariantSchema.make({ - variants: ["select", "insert", "update", "json", "jsonCreate", "jsonUpdate"], - defaultVariant: "select", - }) +const { Class, Field, FieldExcept, FieldOnly, Struct, Union, extract, fieldEvolve } = VariantSchema.make({ + variants: ["select", "insert", "update", "json", "jsonCreate", "jsonUpdate"], + defaultVariant: "select", +}) export type Any = Schema.Top & { readonly fields: Schema.Struct.Fields @@ -70,18 +69,14 @@ export const fields: >(self: A) => A[typeof export const Override: (value: A) => A & Brand<"Override"> = VariantSchema.Override -export interface Generated< - S extends Schema.Top, -> extends VariantSchema.Field<{ +export interface Generated extends VariantSchema.Field<{ readonly select: S readonly update: S readonly json: S }> {} /** A field for database-generated columns (available for select and update, not insert). */ -export const Generated = ( - schema: S, -): Generated => +export const Generated = (schema: S): Generated => Field({ select: schema, update: schema, @@ -111,9 +106,7 @@ export const GeneratedOptional = (schema: S): GeneratedOpt jsonCreate: Schema.optionalKey(schema), }) -export interface GeneratedByApp< - S extends Schema.Top, -> extends VariantSchema.Field<{ +export interface GeneratedByApp extends VariantSchema.Field<{ readonly select: S readonly insert: S readonly update: S @@ -121,9 +114,7 @@ export interface GeneratedByApp< }> {} /** A field for application-generated columns (required for DB variants, optional for JSON). */ -export const GeneratedByApp = ( - schema: S, -): GeneratedByApp => +export const GeneratedByApp = (schema: S): GeneratedByApp => Field({ select: schema, insert: schema, @@ -131,18 +122,14 @@ export const GeneratedByApp = ( json: schema, }) -export interface Sensitive< - S extends Schema.Top, -> extends VariantSchema.Field<{ +export interface Sensitive extends VariantSchema.Field<{ readonly select: S readonly insert: S readonly update: S }> {} /** A field for sensitive values hidden from JSON variants. */ -export const Sensitive = ( - schema: S, -): Sensitive => +export const Sensitive = (schema: S): Sensitive => Field({ select: schema, insert: schema, @@ -184,10 +171,7 @@ export interface DateTimeFromDate extends Schema.DateTimeUtcFromDate {} export const DateTimeFromDate: DateTimeFromDate = Schema.DateTimeUtcFromDate -export interface Date extends Schema.decodeTo< - Schema.DateTimeUtc, - Schema.String -> {} +export interface Date extends Schema.decodeTo {} /** A DateTime.Utc serialized as ISO date string (YYYY-MM-DD). */ export const Date: Date = Schema.String.pipe( @@ -197,7 +181,9 @@ export const Date: Date = Schema.String.pipe( if (opt._tag === "Some") { return Effect.succeed(DateTime.removeTime(opt.value)) } - return Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: "Invalid date format" })) + return Effect.fail( + new SchemaIssue.InvalidValue(Option.some(s), { message: "Invalid date format" }), + ) }), encode: SchemaGetter.transform((dt: DateTime.Utc) => DateTime.formatIsoDate(dt)), }), @@ -211,19 +197,13 @@ export const DateTimeWithNow = VariantSchema.Overrideable(Schema.DateTimeUtcFrom defaultValue: DateTime.now, }) -export const DateTimeFromDateWithNow = VariantSchema.Overrideable( - Schema.DateTimeUtcFromDate, - { - defaultValue: DateTime.now, - }, -) +export const DateTimeFromDateWithNow = VariantSchema.Overrideable(Schema.DateTimeUtcFromDate, { + defaultValue: DateTime.now, +}) -export const DateTimeFromNumberWithNow = VariantSchema.Overrideable( - Schema.DateTimeUtcFromMillis, - { - defaultValue: DateTime.now, - }, -) +export const DateTimeFromNumberWithNow = VariantSchema.Overrideable(Schema.DateTimeUtcFromMillis, { + defaultValue: DateTime.now, +}) export interface DateTimeInsert extends VariantSchema.Field<{ readonly select: typeof Schema.DateTimeUtcFromString @@ -309,9 +289,7 @@ export const DateTimeUpdateFromNumber: DateTimeUpdateFromNumber = Field({ json: Schema.DateTimeUtcFromMillis, }) -export interface JsonFromString< - S extends Schema.Top, -> extends VariantSchema.Field<{ +export interface JsonFromString extends VariantSchema.Field<{ readonly select: Schema.fromJsonString readonly insert: Schema.fromJsonString readonly update: Schema.fromJsonString @@ -321,9 +299,7 @@ export interface JsonFromString< }> {} /** A JSON value stored as text in the database, object in JSON variants. */ -export const JsonFromString = ( - schema: S, -): JsonFromString => { +export const JsonFromString = (schema: S): JsonFromString => { const parsed = Schema.fromJsonString(schema) return Field({ select: parsed, diff --git a/packages/domain/src/rpc/bots.ts b/packages/domain/src/rpc/bots.ts index 6aff2c04e..46e81b188 100644 --- a/packages/domain/src/rpc/bots.ts +++ b/packages/domain/src/rpc/bots.ts @@ -179,7 +179,12 @@ export class BotRpcs extends RpcGroup.make( isPublic: Schema.optional(Schema.Boolean), }), success: BotResponse, - error: Schema.Union([BotNotFoundError, UnauthorizedError, InternalServerError, RateLimitExceededError]), + error: Schema.Union([ + BotNotFoundError, + UnauthorizedError, + InternalServerError, + RateLimitExceededError, + ]), }) .annotate(RequiredScopes, ["bots:write"]) .middleware(AuthMiddleware), @@ -216,7 +221,12 @@ export class BotRpcs extends RpcGroup.make( Rpc.make("bot.regenerateToken", { payload: Schema.Struct({ id: BotId }), success: BotCreatedResponse, - error: Schema.Union([BotNotFoundError, UnauthorizedError, InternalServerError, RateLimitExceededError]), + error: Schema.Union([ + BotNotFoundError, + UnauthorizedError, + InternalServerError, + RateLimitExceededError, + ]), }) .annotate(RequiredScopes, ["bots:write"]) .middleware(AuthMiddleware), @@ -315,7 +325,12 @@ export class BotRpcs extends RpcGroup.make( Rpc.make("bot.uninstall", { payload: Schema.Struct({ botId: BotId }), success: Schema.Struct({ transactionId: TransactionId }), - error: Schema.Union([BotNotFoundError, UnauthorizedError, InternalServerError, RateLimitExceededError]), + error: Schema.Union([ + BotNotFoundError, + UnauthorizedError, + InternalServerError, + RateLimitExceededError, + ]), }) .annotate(RequiredScopes, ["bots:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/channel-members.ts b/packages/domain/src/rpc/channel-members.ts index c1c86f177..34b50dec9 100644 --- a/packages/domain/src/rpc/channel-members.ts +++ b/packages/domain/src/rpc/channel-members.ts @@ -102,7 +102,10 @@ export class ChannelMemberRpcs extends RpcGroup.make( Rpc.make("channelMember.update", { payload: Schema.Struct({ id: ChannelMemberId, - }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(ChannelMember.Model.jsonUpdate as any).fields }) as any), + }).pipe( + (s: any) => + Schema.Struct({ ...s.fields, ...(ChannelMember.Model.jsonUpdate as any).fields }) as any, + ), success: ChannelMemberResponse, error: Schema.Union([ChannelMemberNotFoundError, UnauthorizedError, InternalServerError]), }) diff --git a/packages/domain/src/rpc/channel-sections.ts b/packages/domain/src/rpc/channel-sections.ts index 184b32b29..44e750e3a 100644 --- a/packages/domain/src/rpc/channel-sections.ts +++ b/packages/domain/src/rpc/channel-sections.ts @@ -70,7 +70,10 @@ export class ChannelSectionRpcs extends RpcGroup.make( Rpc.make("channelSection.update", { payload: Schema.Struct({ id: ChannelSectionId, - }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(ChannelSection.Model.jsonUpdate as any).fields }) as any), + }).pipe( + (s: any) => + Schema.Struct({ ...s.fields, ...(ChannelSection.Model.jsonUpdate as any).fields }) as any, + ), success: ChannelSectionResponse, error: Schema.Union([ChannelSectionNotFoundError, UnauthorizedError, InternalServerError]), }) diff --git a/packages/domain/src/rpc/channels.ts b/packages/domain/src/rpc/channels.ts index d797a5dd5..354b54bba 100644 --- a/packages/domain/src/rpc/channels.ts +++ b/packages/domain/src/rpc/channels.ts @@ -36,9 +36,12 @@ export class ChannelResponse extends Schema.Class("ChannelRespo * Error thrown when a channel is not found. * Used in update and delete operations. */ -export class ChannelNotFoundError extends Schema.TaggedErrorClass()("ChannelNotFoundError", { - channelId: ChannelId, -}) {} +export class ChannelNotFoundError extends Schema.TaggedErrorClass()( + "ChannelNotFoundError", + { + channelId: ChannelId, + }, +) {} /** * Request schema for creating DM or group channels. @@ -109,7 +112,9 @@ export class ChannelRpcs extends RpcGroup.make( Rpc.make("channel.update", { payload: Schema.Struct({ id: ChannelId, - }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(Channel.Model.jsonUpdate as any).fields }) as any), + }).pipe( + (s: any) => Schema.Struct({ ...s.fields, ...(Channel.Model.jsonUpdate as any).fields }) as any, + ), success: ChannelResponse, error: Schema.Union([ChannelNotFoundError, UnauthorizedError, InternalServerError]), }) @@ -174,7 +179,12 @@ export class ChannelRpcs extends RpcGroup.make( Rpc.make("channel.createThread", { payload: CreateThreadRequest, success: ChannelResponse, - error: Schema.Union([MessageNotFoundError, NestedThreadError, UnauthorizedError, InternalServerError]), + error: Schema.Union([ + MessageNotFoundError, + NestedThreadError, + UnauthorizedError, + InternalServerError, + ]), }) .annotate(RequiredScopes, ["channels:write"]) .middleware(AuthMiddleware), diff --git a/packages/domain/src/rpc/middleware.ts b/packages/domain/src/rpc/middleware.ts index 2699f42a3..4327116d2 100644 --- a/packages/domain/src/rpc/middleware.ts +++ b/packages/domain/src/rpc/middleware.ts @@ -54,9 +54,12 @@ const AuthFailure = S.Union([ WorkOSUserFetchError, ]) -export class AuthMiddleware extends RpcMiddleware.Service()("AuthMiddleware", { +export class AuthMiddleware extends RpcMiddleware.Service< + AuthMiddleware, + { + provides: CurrentUser.Context + } +>()("AuthMiddleware", { error: AuthFailure, requiredForClient: true, }) {} diff --git a/packages/domain/src/rpc/organizations.ts b/packages/domain/src/rpc/organizations.ts index 5003aab69..d3d3e7a32 100644 --- a/packages/domain/src/rpc/organizations.ts +++ b/packages/domain/src/rpc/organizations.ts @@ -80,7 +80,10 @@ export class OrganizationRpcs extends RpcGroup.make( Rpc.make("organization.update", { payload: Schema.Struct({ id: OrganizationId, - }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(Organization.Model.jsonUpdate as any).fields }) as any), + }).pipe( + (s: any) => + Schema.Struct({ ...s.fields, ...(Organization.Model.jsonUpdate as any).fields }) as any, + ), success: OrganizationResponse, error: Schema.Union([ OrganizationNotFoundError, diff --git a/packages/domain/src/scopes/current-bot-scopes.ts b/packages/domain/src/scopes/current-bot-scopes.ts index dd5742480..fd14d2b92 100644 --- a/packages/domain/src/scopes/current-bot-scopes.ts +++ b/packages/domain/src/scopes/current-bot-scopes.ts @@ -11,6 +11,7 @@ import type { ApiScope } from "./api-scope" * When Option.none (default), OrgResolver falls back to role-based scopes * for normal (human) users. */ -export class CurrentBotScopes extends ServiceMap.Service>>()( - "CurrentBotScopes", -) {} +export class CurrentBotScopes extends ServiceMap.Service< + CurrentBotScopes, + Option.Option> +>()("CurrentBotScopes") {} diff --git a/packages/domain/src/scopes/required-scopes.ts b/packages/domain/src/scopes/required-scopes.ts index 6b4c1c60c..f23194b90 100644 --- a/packages/domain/src/scopes/required-scopes.ts +++ b/packages/domain/src/scopes/required-scopes.ts @@ -14,6 +14,6 @@ import type { ApiScope } from "./api-scope" * - Empty array `[]` = public endpoint (no scope needed) * - Missing annotation = error (caught by startup validation) */ -export class RequiredScopes extends ServiceMap.Service ->()("@hazel/domain/RequiredScopes") {} +export class RequiredScopes extends ServiceMap.Service>()( + "@hazel/domain/RequiredScopes", +) {} diff --git a/packages/domain/src/scopes/scope-map.ts b/packages/domain/src/scopes/scope-map.ts index 3603072f5..2dcfc3137 100644 --- a/packages/domain/src/scopes/scope-map.ts +++ b/packages/domain/src/scopes/scope-map.ts @@ -18,7 +18,9 @@ export const scopeMapFromRpcGroup = ( ): ScopeMap => { const map: Record> = {} for (const [tag, rpc] of requests) { - const scopes = ServiceMap.get(rpc.annotations as any, RequiredScopes) as ReadonlyArray | undefined + const scopes = ServiceMap.get(rpc.annotations as any, RequiredScopes) as + | ReadonlyArray + | undefined if (scopes) { map[tag] = scopes } diff --git a/packages/domain/src/scopes/validate-scopes.ts b/packages/domain/src/scopes/validate-scopes.ts index df18d4632..66cf7ac48 100644 --- a/packages/domain/src/scopes/validate-scopes.ts +++ b/packages/domain/src/scopes/validate-scopes.ts @@ -11,7 +11,9 @@ export const validateRpcGroupScopes = ( ): { valid: boolean; missing: string[] } => { const missing: string[] = [] for (const [tag, rpc] of requests) { - const scopes = ServiceMap.get(rpc.annotations as any, RequiredScopes) as ReadonlyArray | undefined + const scopes = ServiceMap.get(rpc.annotations as any, RequiredScopes) as + | ReadonlyArray + | undefined if (!scopes) { missing.push(`${groupName}.${tag}`) } diff --git a/packages/domain/src/session-errors.ts b/packages/domain/src/session-errors.ts index 4c8d72530..e60a654d6 100644 --- a/packages/domain/src/session-errors.ts +++ b/packages/domain/src/session-errors.ts @@ -73,7 +73,9 @@ export class SessionRefreshError extends Schema.TaggedErrorClass("WorkOSUserFetchError")( +export class WorkOSUserFetchError extends Schema.TaggedErrorClass( + "WorkOSUserFetchError", +)( "WorkOSUserFetchError", { message: Schema.String, diff --git a/packages/effect-bun/src/Redis.ts b/packages/effect-bun/src/Redis.ts index 46a0d5300..0fef120fd 100644 --- a/packages/effect-bun/src/Redis.ts +++ b/packages/effect-bun/src/Redis.ts @@ -92,7 +92,8 @@ const sanitizeRedisUrl = (url: string): string => url.replace(/\/\/.*@/, "//***@ * }).pipe(Effect.provide(Redis.layer)) * ``` */ -export class Redis extends ServiceMap.Service - Effect.fail(new RedisError({ - message: `Redis connection timed out after 10s (url: ${sanitizeRedisUrl(url)})`, - })), + Effect.fail( + new RedisError({ + message: `Redis connection timed out after 10s (url: ${sanitizeRedisUrl(url)})`, + }), + ), }), ) @@ -273,9 +276,11 @@ export class Redis extends ServiceMap.Service - Effect.fail(new RedisError({ - message: `Redis connection timed out after 10s (url: ${sanitizeRedisUrl(url)})`, - })), + Effect.fail( + new RedisError({ + message: `Redis connection timed out after 10s (url: ${sanitizeRedisUrl(url)})`, + }), + ), }), ) diff --git a/packages/effect-bun/src/S3.ts b/packages/effect-bun/src/S3.ts index f0ebd9a9f..b8581ee99 100644 --- a/packages/effect-bun/src/S3.ts +++ b/packages/effect-bun/src/S3.ts @@ -28,9 +28,12 @@ export class S3MissingCredentialsError extends Schema.TaggedErrorClass()("S3InvalidMethodError", { - message: Schema.String, -}) {} +export class S3InvalidMethodError extends Schema.TaggedErrorClass()( + "S3InvalidMethodError", + { + message: Schema.String, + }, +) {} /** * Invalid S3 path/key @@ -138,7 +141,8 @@ export type S3WriteData = string | ArrayBuffer | Uint8Array | Blob | Response | * }) * ``` */ -export class S3 extends ServiceMap.Service "deployment.commit_sha": commitSha, }, }, - }).pipe( - Layer.provide(FetchHttpClient.layer), - Layer.provide(OtlpSerialization.layerJson), - ) + }).pipe(Layer.provide(FetchHttpClient.layer), Layer.provide(OtlpSerialization.layerJson)) }), ) diff --git a/packages/effect-bun/src/persistence/redis-backing.ts b/packages/effect-bun/src/persistence/redis-backing.ts index c27ab42ce..1ab3e3c9e 100644 --- a/packages/effect-bun/src/persistence/redis-backing.ts +++ b/packages/effect-bun/src/persistence/redis-backing.ts @@ -21,39 +21,31 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { Effect.sync(() => { const prefixed = (key: string) => `${prefix}:${key}` - const parse = (method: string) => (str: string | null): Effect.Effect => { - if (str === null) return Effect.succeed(undefined) - return Effect.try({ - try: () => JSON.parse(str) as object, - catch: (error) => makePersistenceError(method, error), - }) - } + const parse = + (method: string) => + (str: string | null): Effect.Effect => { + if (str === null) return Effect.succeed(undefined) + return Effect.try({ + try: () => JSON.parse(str) as object, + catch: (error) => makePersistenceError(method, error), + }) + } return identity({ get: (key) => Effect.flatMap( redis .get(prefixed(key)) - .pipe( - Effect.mapError((error) => - makePersistenceError("get", error), - ), - ), + .pipe(Effect.mapError((error) => makePersistenceError("get", error))), parse("get"), ), getMany: (keys) => - redis - .send<(string | null)[]>("MGET", keys.map(prefixed)) - .pipe( - Effect.mapError((error) => - makePersistenceError("getMany", error), - ), - Effect.flatMap((results) => - Effect.forEach(results, parse("getMany")), - ), - Effect.map((results) => results as any), - ), + redis.send<(string | null)[]>("MGET", keys.map(prefixed)).pipe( + Effect.mapError((error) => makePersistenceError("getMany", error)), + Effect.flatMap((results) => Effect.forEach(results, parse("getMany"))), + Effect.map((results) => results as any), + ), set: (key, value, ttl) => Effect.gen(function* () { @@ -66,25 +58,12 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { if (ttl !== undefined) { // Atomic SET with PX (milliseconds) - sets value and TTL in single command yield* redis - .send("SET", [ - pkey, - serialized, - "PX", - String(Duration.toMillis(ttl)), - ]) - .pipe( - Effect.mapError((error) => - makePersistenceError("set", error), - ), - ) + .send("SET", [pkey, serialized, "PX", String(Duration.toMillis(ttl))]) + .pipe(Effect.mapError((error) => makePersistenceError("set", error))) } else { yield* redis .set(pkey, serialized) - .pipe( - Effect.mapError((error) => - makePersistenceError("set", error), - ), - ) + .pipe(Effect.mapError((error) => makePersistenceError("set", error))) } }), @@ -95,12 +74,7 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { const serialized = JSON.stringify(value) if (ttl !== undefined) { yield* redis - .send("SET", [ - pkey, - serialized, - "PX", - String(Duration.toMillis(ttl)), - ]) + .send("SET", [pkey, serialized, "PX", String(Duration.toMillis(ttl))]) .pipe( Effect.mapError((error) => makePersistenceError("setMany", error), @@ -121,28 +95,16 @@ export const makeRedisBackingPersistence = Effect.gen(function* () { remove: (key) => redis .del(prefixed(key)) - .pipe( - Effect.mapError((error) => - makePersistenceError("remove", error), - ), - ), + .pipe(Effect.mapError((error) => makePersistenceError("remove", error))), clear: Effect.gen(function* () { const keys = yield* redis .send("KEYS", [`${prefix}:*`]) - .pipe( - Effect.mapError((error) => - makePersistenceError("clear", error), - ), - ) + .pipe(Effect.mapError((error) => makePersistenceError("clear", error))) if (keys.length > 0) { yield* redis .send("DEL", keys) - .pipe( - Effect.mapError((error) => - makePersistenceError("clear", error), - ), - ) + .pipe(Effect.mapError((error) => makePersistenceError("clear", error))) } }), }) @@ -165,9 +127,7 @@ export const RedisBackingPersistenceLive = Layer.effect( * Requires: Redis * Provides: Persistence.Persistence */ -export const RedisResultPersistenceLive = Persistence.layer.pipe( - Layer.provide(RedisBackingPersistenceLive), -) +export const RedisResultPersistenceLive = Persistence.layer.pipe(Layer.provide(RedisBackingPersistenceLive)) /** * In-memory persistence layer for testing or fallback. diff --git a/packages/integrations/src/craft/api-client.ts b/packages/integrations/src/craft/api-client.ts index fa966a1cd..e16e9882c 100644 --- a/packages/integrations/src/craft/api-client.ts +++ b/packages/integrations/src/craft/api-client.ts @@ -127,10 +127,13 @@ export class CraftNotFoundError extends Schema.TaggedErrorClass()("CraftRateLimitError", { - message: Schema.String, - retryAfter: Schema.optional(Schema.Number), -}) {} +export class CraftRateLimitError extends Schema.TaggedErrorClass()( + "CraftRateLimitError", + { + message: Schema.String, + retryAfter: Schema.optional(Schema.Number), + }, +) {} // ============================================================================ // Internal Response Schemas @@ -307,22 +310,16 @@ export class CraftApiClient extends ServiceMap.Service()("CraftA return yield* response.json }).pipe( - Effect.catchTag("TimeoutException", () => + Effect.catchTag("TimeoutError", () => Effect.fail(new CraftApiError({ message: "Request timed out" })), ), - Effect.catchTag("RequestError", (error) => - Effect.fail( - new CraftApiError({ - message: `Network error: ${String(error)}`, - cause: error, - }), - ), - ), - Effect.catchTag("ResponseError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new CraftApiError({ - message: `Response error: ${String(error)}`, - status: error.response.status, + message: error.response + ? `Response error: ${String(error)}` + : `Network error: ${String(error)}`, + status: error.response?.status, cause: error, }), ), @@ -338,7 +335,7 @@ export class CraftApiClient extends ServiceMap.Service()("CraftA const client = makeClient(baseUrl, accessToken) return yield* executeRequest(client, "GET", "/connection").pipe( Effect.flatMap((raw) => - Schema.decodeUnknown(ConnectionInfoResponse)(raw).pipe( + Schema.decodeUnknownEffect(ConnectionInfoResponse)(raw).pipe( Effect.map(normalizeCraftConnectionInfo), Effect.mapError( (e) => @@ -366,7 +363,7 @@ export class CraftApiClient extends ServiceMap.Service()("CraftA const path = `/blocks?id=${encodeURIComponent(resolvedBlockId)}` const raw = yield* executeRequest(client, "GET", path) const normalizedBlocks = normalizeCraftItemsResponse(raw) - return yield* Schema.decodeUnknown(Schema.Array(CraftBlock))(normalizedBlocks).pipe( + return yield* Schema.decodeUnknownEffect(Schema.Array(CraftBlock))(normalizedBlocks).pipe( Effect.catch(() => Effect.succeed( normalizedBlocks.length > 0 @@ -494,9 +491,9 @@ export class CraftApiClient extends ServiceMap.Service()("CraftA const path = folderId ? `/documents?folderId=${encodeURIComponent(folderId)}` : "/documents" const raw = yield* executeRequest(client, "GET", path) const normalizedDocuments = normalizeCraftItemsResponse(raw) - return yield* Schema.decodeUnknown(Schema.Array(CraftDocument))(normalizedDocuments).pipe( - Effect.catch(() => Effect.succeed(normalizedDocuments as CraftDocument[])), - ) + return yield* Schema.decodeUnknownEffect(Schema.Array(CraftDocument))( + normalizedDocuments, + ).pipe(Effect.catch(() => Effect.succeed(normalizedDocuments as CraftDocument[]))) }).pipe( Effect.retry({ schedule: makeRetrySchedule, while: isRetryableError }), Effect.withSpan("CraftApiClient.listDocuments"), @@ -596,7 +593,7 @@ export class CraftApiClient extends ServiceMap.Service()("CraftA const client = makeClient(baseUrl, accessToken) const raw = yield* executeRequest(client, "GET", "/folders") const normalizedFolders = normalizeCraftItemsResponse(raw) - return yield* Schema.decodeUnknown(Schema.Array(CraftFolder))(normalizedFolders).pipe( + return yield* Schema.decodeUnknownEffect(Schema.Array(CraftFolder))(normalizedFolders).pipe( Effect.catch(() => Effect.succeed(normalizedFolders as CraftFolder[])), ) }).pipe( @@ -665,7 +662,7 @@ export class CraftApiClient extends ServiceMap.Service()("CraftA const path = `/tasks?scope=${resolvedScope}` const raw = yield* executeRequest(client, "GET", path) const normalizedTasks = normalizeCraftItemsResponse(raw) - return yield* Schema.decodeUnknown(Schema.Array(CraftTask))(normalizedTasks).pipe( + return yield* Schema.decodeUnknownEffect(Schema.Array(CraftTask))(normalizedTasks).pipe( Effect.catch(() => Effect.succeed(normalizedTasks as CraftTask[])), ) }).pipe( @@ -735,9 +732,9 @@ export class CraftApiClient extends ServiceMap.Service()("CraftA const client = makeClient(baseUrl, accessToken) const raw = yield* executeRequest(client, "GET", "/collections") const normalizedCollections = normalizeCraftItemsResponse(raw) - return yield* Schema.decodeUnknown(Schema.Array(CraftCollection))(normalizedCollections).pipe( - Effect.catch(() => Effect.succeed(normalizedCollections as CraftCollection[])), - ) + return yield* Schema.decodeUnknownEffect(Schema.Array(CraftCollection))( + normalizedCollections, + ).pipe(Effect.catch(() => Effect.succeed(normalizedCollections as CraftCollection[]))) }).pipe( Effect.retry({ schedule: makeRetrySchedule, while: isRetryableError }), Effect.withSpan("CraftApiClient.listCollections"), @@ -870,7 +867,5 @@ export class CraftApiClient extends ServiceMap.Service()("CraftA } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(FetchHttpClient.layer), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(FetchHttpClient.layer)) } diff --git a/packages/integrations/src/discord/api-client.ts b/packages/integrations/src/discord/api-client.ts index 09ed59131..b36f35356 100644 --- a/packages/integrations/src/discord/api-client.ts +++ b/packages/integrations/src/discord/api-client.ts @@ -1,5 +1,5 @@ import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http" -import { ServiceMap, Duration, Effect, Layer, Schema } from "effect" +import { ServiceMap, Duration, Effect, Layer, Result, Schema } from "effect" export const DiscordAccountInfo = Schema.Struct({ externalAccountId: Schema.String, @@ -31,14 +31,14 @@ const DiscordUserApiResponse = Schema.Struct({ id: Schema.String, username: Schema.String, global_name: Schema.optional(Schema.NullOr(Schema.String)), - discriminator: Schema.optional(Schema.String, { default: () => "0" }), + discriminator: Schema.String.pipe(Schema.withDecodingDefaultKey(() => "0")), }) const DiscordGuildApiResponse = Schema.Struct({ id: Schema.String, name: Schema.String, icon: Schema.optional(Schema.NullOr(Schema.String)), - owner: Schema.optional(Schema.Boolean, { default: () => false }), + owner: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(() => false)), }) const DiscordWebhookCreateResponse = Schema.Struct({ @@ -59,7 +59,7 @@ const DiscordMessageCreateResponse = Schema.Struct({ }) const DiscordErrorApiResponse = Schema.Struct({ - message: Schema.optional(Schema.String, { default: () => "Unknown Discord error" }), + message: Schema.String.pipe(Schema.withDecodingDefaultKey(() => "Unknown Discord error")), }) export class DiscordApiError extends Schema.TaggedErrorClass()("DiscordApiError", { @@ -115,16 +115,16 @@ export class DiscordApiClient extends ServiceMap.Service()("Di status: number json: Effect.Effect }) { - const bodyResult = yield* response.json.pipe(Effect.either) - const message = - bodyResult._tag === "Right" - ? (() => { - const decoded = Schema.decodeUnknownSync(DiscordErrorApiResponse)( - bodyResult.right, - ) - return parseDiscordErrorMessage(response.status, decoded.message) - })() - : `Discord API request failed with status ${response.status}` + const bodyResult = yield* response.json.pipe(Effect.result) + const message = Result.isSuccess(bodyResult) + ? (() => { + const decoded = Schema.decodeUnknownSync(DiscordErrorApiResponse)(bodyResult.success) + return parseDiscordErrorMessage( + response.status, + decoded.message ?? "Unknown Discord error", + ) + })() + : `Discord API request failed with status ${response.status}` return yield* Effect.fail( new DiscordApiError({ @@ -144,7 +144,7 @@ export class DiscordApiClient extends ServiceMap.Service()("Di } const body = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(DiscordUserApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(DiscordUserApiResponse)), Effect.mapError( (cause) => new DiscordApiError({ @@ -174,7 +174,7 @@ export class DiscordApiClient extends ServiceMap.Service()("Di } const body = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(Schema.Array(DiscordGuildApiResponse))), + Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(DiscordGuildApiResponse))), Effect.mapError( (cause) => new DiscordApiError({ @@ -207,7 +207,7 @@ export class DiscordApiClient extends ServiceMap.Service()("Di } const body = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(Schema.Array(DiscordGuildChannelApiResponse))), + Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(DiscordGuildChannelApiResponse))), Effect.mapError( (cause) => new DiscordApiError({ @@ -266,7 +266,7 @@ export class DiscordApiClient extends ServiceMap.Service()("Di } const body = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(DiscordMessageCreateResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(DiscordMessageCreateResponse)), Effect.mapError( (cause) => new DiscordApiError({ @@ -302,7 +302,7 @@ export class DiscordApiClient extends ServiceMap.Service()("Di } const body = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(DiscordWebhookCreateResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(DiscordWebhookCreateResponse)), Effect.mapError( (cause) => new DiscordApiError({ @@ -365,7 +365,7 @@ export class DiscordApiClient extends ServiceMap.Service()("Di } const body = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(DiscordMessageCreateResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(DiscordMessageCreateResponse)), Effect.mapError( (cause) => new DiscordApiError({ @@ -518,7 +518,7 @@ export class DiscordApiClient extends ServiceMap.Service()("Di } const body = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(DiscordMessageCreateResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(DiscordMessageCreateResponse)), Effect.mapError( (cause) => new DiscordApiError({ @@ -548,7 +548,5 @@ export class DiscordApiClient extends ServiceMap.Service()("Di } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(FetchHttpClient.layer), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(FetchHttpClient.layer)) } diff --git a/packages/integrations/src/github/api-client.ts b/packages/integrations/src/github/api-client.ts index f8fc3f83a..96840ff07 100644 --- a/packages/integrations/src/github/api-client.ts +++ b/packages/integrations/src/github/api-client.ts @@ -114,10 +114,13 @@ export class GitHubPRNotFoundError extends Schema.TaggedErrorClass()("GitHubRateLimitError", { - message: Schema.String, - retryAfter: Schema.optional(Schema.Number), // seconds until rate limit resets -}) {} +export class GitHubRateLimitError extends Schema.TaggedErrorClass()( + "GitHubRateLimitError", + { + message: Schema.String, + retryAfter: Schema.optional(Schema.Number), // seconds until rate limit resets + }, +) {} // ============================================================================ // GitHub API Response Schemas (internal, for validation) @@ -129,27 +132,24 @@ const GitHubPRApiResponse = Schema.Struct({ title: Schema.String, body: Schema.NullOr(Schema.String), state: Schema.String, - draft: Schema.optional(Schema.Boolean, { default: () => false }), - merged: Schema.optional(Schema.Boolean, { default: () => false }), + draft: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(() => false)), + merged: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(() => false)), user: Schema.NullOr( Schema.Struct({ login: Schema.String, avatar_url: Schema.optional(Schema.NullOr(Schema.String)), }), ), - additions: Schema.optional(Schema.Number, { default: () => 0 }), - deletions: Schema.optional(Schema.Number, { default: () => 0 }), + additions: Schema.Number.pipe(Schema.withDecodingDefaultKey(() => 0)), + deletions: Schema.Number.pipe(Schema.withDecodingDefaultKey(() => 0)), head: Schema.optional(Schema.Struct({ ref: Schema.String })), updated_at: Schema.optional(Schema.String), - labels: Schema.optionalWith( - Schema.Array( - Schema.Struct({ - name: Schema.String, - color: Schema.String, - }), - ), - { default: () => [] }, - ), + labels: Schema.Array( + Schema.Struct({ + name: Schema.String, + color: Schema.String, + }), + ).pipe(Schema.withDecodingDefaultKey(() => [])), }) // GitHub API repository owner response schema @@ -178,13 +178,13 @@ const GitHubRepositoriesApiResponse = Schema.Struct({ // GitHub API error response schema const GitHubErrorApiResponse = Schema.Struct({ - message: Schema.optional(Schema.String, { default: () => "Unknown error" }), + message: Schema.String.pipe(Schema.withDecodingDefaultKey(() => "Unknown error")), }) // GitHub App info response schema const GitHubAppApiResponse = Schema.Struct({ id: Schema.Number, - name: Schema.optional(Schema.String, { default: () => "GitHub App" }), + name: Schema.String.pipe(Schema.withDecodingDefaultKey(() => "GitHub App")), }) // ============================================================================ @@ -376,7 +376,7 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH // Handle other error status codes if (response.status >= 400) { const errorBody = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(GitHubErrorApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(GitHubErrorApiResponse)), Effect.catch((error) => Effect.logDebug(`Failed to parse GitHub error response: ${String(error)}`).pipe( Effect.as({ message: "Unknown error" }), @@ -389,7 +389,7 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH // Parse successful response const prData = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(GitHubPRApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(GitHubPRApiResponse)), Effect.mapError( (error) => new GitHubApiError({ @@ -461,7 +461,7 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH // Handle 403 which GitHub uses for rate limiting on unauthenticated requests if (response.status === 403) { const errorBody = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(GitHubErrorApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(GitHubErrorApiResponse)), Effect.catch(() => Effect.succeed({ message: "" })), ) if (errorBody.message.toLowerCase().includes("rate limit")) { @@ -480,7 +480,7 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH // Handle other error status codes if (response.status >= 400) { const errorBody = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(GitHubErrorApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(GitHubErrorApiResponse)), Effect.catch((error) => Effect.logDebug(`Failed to parse GitHub error response: ${String(error)}`).pipe( Effect.as({ message: "Unknown error" }), @@ -493,7 +493,7 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH // Parse successful response const prData = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(GitHubPRApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(GitHubPRApiResponse)), Effect.mapError( (error) => new GitHubApiError({ @@ -558,7 +558,7 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH // Handle error status codes if (response.status >= 400) { const errorBody = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(GitHubErrorApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(GitHubErrorApiResponse)), Effect.catch((error) => Effect.logDebug(`Failed to parse GitHub error response: ${String(error)}`).pipe( Effect.as({ message: "Unknown error" }), @@ -575,7 +575,7 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH // Parse successful response const data = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(GitHubRepositoriesApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(GitHubRepositoriesApiResponse)), Effect.mapError( (error) => new GitHubApiError({ @@ -638,7 +638,7 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH if (reposResponse.status >= 200 && reposResponse.status < 300) { const data = yield* reposResponse.json.pipe( - Effect.flatMap(Schema.decodeUnknown(GitHubRepositoriesApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(GitHubRepositoriesApiResponse)), Effect.catch((error) => Effect.logDebug( `Failed to parse GitHub repositories response: ${String(error)}`, @@ -674,7 +674,7 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH if (appResponse.status >= 200 && appResponse.status < 300) { const appData = yield* appResponse.json.pipe( - Effect.flatMap(Schema.decodeUnknown(GitHubAppApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(GitHubAppApiResponse)), Effect.catch((error) => Effect.logDebug(`Failed to parse GitHub App response: ${String(error)}`).pipe( Effect.as({ id: 0, name: "GitHub App" }), @@ -698,22 +698,16 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH // Apply error handling, retry, and spans to each method const wrappedFetchPR = (owner: string, repo: string, prNumber: number, accessToken: string) => fetchPR(owner, repo, prNumber, accessToken).pipe( - Effect.catchTag("TimeoutException", () => + Effect.catchTag("TimeoutError", () => Effect.fail(new GitHubApiError({ message: "Request timed out" })), ), - Effect.catchTag("RequestError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new GitHubApiError({ - message: `Network error: ${String(error)}`, - cause: error, - }), - ), - ), - Effect.catchTag("ResponseError", (error) => - Effect.fail( - new GitHubApiError({ - message: `Response error: ${String(error)}`, - status: error.response.status, + message: error.response + ? `Response error: ${String(error)}` + : `Network error: ${String(error)}`, + status: error.response?.status, cause: error, }), ), @@ -727,22 +721,16 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH const wrappedFetchPRPublic = (owner: string, repo: string, prNumber: number) => fetchPRPublic(owner, repo, prNumber).pipe( - Effect.catchTag("TimeoutException", () => + Effect.catchTag("TimeoutError", () => Effect.fail(new GitHubApiError({ message: "Request timed out" })), ), - Effect.catchTag("RequestError", (error) => - Effect.fail( - new GitHubApiError({ - message: `Network error: ${String(error)}`, - cause: error, - }), - ), - ), - Effect.catchTag("ResponseError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new GitHubApiError({ - message: `Response error: ${String(error)}`, - status: error.response.status, + message: error.response + ? `Response error: ${String(error)}` + : `Network error: ${String(error)}`, + status: error.response?.status, cause: error, }), ), @@ -756,22 +744,16 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH const wrappedFetchRepositories = (accessToken: string, page: number, perPage: number) => fetchRepositories(accessToken, page, perPage).pipe( - Effect.catchTag("TimeoutException", () => + Effect.catchTag("TimeoutError", () => Effect.fail(new GitHubApiError({ message: "Request timed out" })), ), - Effect.catchTag("RequestError", (error) => - Effect.fail( - new GitHubApiError({ - message: `Network error: ${String(error)}`, - cause: error, - }), - ), - ), - Effect.catchTag("ResponseError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new GitHubApiError({ - message: `Response error: ${String(error)}`, - status: error.response.status, + message: error.response + ? `Response error: ${String(error)}` + : `Network error: ${String(error)}`, + status: error.response?.status, cause: error, }), ), @@ -788,22 +770,16 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH const wrappedGetAccountInfo = (accessToken: string) => getAccountInfo(accessToken).pipe( - Effect.catchTag("TimeoutException", () => + Effect.catchTag("TimeoutError", () => Effect.fail(new GitHubApiError({ message: "Request timed out" })), ), - Effect.catchTag("RequestError", (error) => - Effect.fail( - new GitHubApiError({ - message: `Network error: ${String(error)}`, - cause: error, - }), - ), - ), - Effect.catchTag("ResponseError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new GitHubApiError({ - message: `Response error: ${String(error)}`, - status: error.response.status, + message: error.response + ? `Response error: ${String(error)}` + : `Network error: ${String(error)}`, + status: error.response?.status, cause: error, }), ), @@ -826,9 +802,7 @@ export class GitHubApiClient extends ServiceMap.Service()("GitH } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(FetchHttpClient.layer), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(FetchHttpClient.layer)) } // ============================================================================ diff --git a/packages/integrations/src/github/jwt-service.ts b/packages/integrations/src/github/jwt-service.ts index b37d00aee..6730c26c7 100644 --- a/packages/integrations/src/github/jwt-service.ts +++ b/packages/integrations/src/github/jwt-service.ts @@ -61,7 +61,7 @@ const InstallationTokenApiResponse = Schema.Struct({ // GitHub API error response schema const GitHubErrorApiResponse = Schema.Struct({ - message: Schema.optional(Schema.String, { default: () => "Unknown error" }), + message: Schema.String.pipe(Schema.withDecodingDefaultKey(() => "Unknown error")), }) // ============================================================================ @@ -208,7 +208,7 @@ export class GitHubAppJWTService extends ServiceMap.Service // Handle error status codes if (response.status >= 400) { const errorBody = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(GitHubErrorApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(GitHubErrorApiResponse)), Effect.catch((error) => Effect.logDebug(`Failed to parse GitHub error response: ${String(error)}`).pipe( Effect.as({ message: "Unknown error" }), @@ -226,7 +226,7 @@ export class GitHubAppJWTService extends ServiceMap.Service // Parse successful response const data = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(InstallationTokenApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(InstallationTokenApiResponse)), Effect.mapError( (error) => new GitHubInstallationTokenError({ @@ -242,21 +242,14 @@ export class GitHubAppJWTService extends ServiceMap.Service expiresAt: new Date(data.expires_at), } satisfies InstallationToken }).pipe( - Effect.catchTag("RequestError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new GitHubInstallationTokenError({ installationId, - message: `Network error: ${String(error)}`, - cause: error, - }), - ), - ), - Effect.catchTag("ResponseError", (error) => - Effect.fail( - new GitHubInstallationTokenError({ - installationId, - message: `Response error: ${String(error)}`, - status: error.response.status, + message: error.response + ? `Response error: ${String(error)}` + : `Network error: ${String(error)}`, + status: error.response?.status, cause: error, }), ), @@ -288,7 +281,5 @@ export class GitHubAppJWTService extends ServiceMap.Service } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(FetchHttpClient.layer), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(FetchHttpClient.layer)) } diff --git a/packages/integrations/src/linear/api-client.ts b/packages/integrations/src/linear/api-client.ts index b53e2b543..25e575673 100644 --- a/packages/integrations/src/linear/api-client.ts +++ b/packages/integrations/src/linear/api-client.ts @@ -93,10 +93,13 @@ export class LinearApiError extends Schema.TaggedErrorClass()("L cause: Schema.optional(Schema.Unknown), }) {} -export class LinearRateLimitError extends Schema.TaggedErrorClass()("LinearRateLimitError", { - message: Schema.String, - retryAfter: Schema.optional(Schema.Number), -}) {} +export class LinearRateLimitError extends Schema.TaggedErrorClass()( + "LinearRateLimitError", + { + message: Schema.String, + retryAfter: Schema.optional(Schema.Number), + }, +) {} export class LinearIssueNotFoundError extends Schema.TaggedErrorClass()( "LinearIssueNotFoundError", @@ -235,7 +238,7 @@ const GraphQLError = Schema.Struct({ }) const GetDefaultTeamResponse = Schema.Struct({ - data: Schema.optionalWith( + data: Schema.OptionFromOptional( Schema.Struct({ teams: Schema.Struct({ nodes: Schema.Array( @@ -246,36 +249,32 @@ const GetDefaultTeamResponse = Schema.Struct({ ), }), }), - { as: "Option" }, ), - errors: Schema.optionalWith(Schema.Array(GraphQLError), { as: "Option" }), + errors: Schema.OptionFromOptional(Schema.Array(GraphQLError)), }) const CreateIssueResponse = Schema.Struct({ - data: Schema.optionalWith( + data: Schema.OptionFromOptional( Schema.Struct({ issueCreate: Schema.Struct({ success: Schema.Boolean, - issue: Schema.optionalWith( + issue: Schema.OptionFromOptional( Schema.Struct({ id: Schema.String, identifier: Schema.String, title: Schema.String, url: Schema.String, - team: Schema.optionalWith( + team: Schema.OptionFromOptional( Schema.Struct({ name: Schema.String, }), - { as: "Option" }, ), }), - { as: "Option" }, ), }), }), - { as: "Option" }, ), - errors: Schema.optionalWith(Schema.Array(GraphQLError), { as: "Option" }), + errors: Schema.OptionFromOptional(Schema.Array(GraphQLError)), }) const GetIssueResponse = Schema.Struct({ @@ -322,13 +321,13 @@ const GetIssueResponse = Schema.Struct({ }), ), }), - null, + { onNoneEncoding: null }, ), - errors: Schema.OptionFromNullishOr(Schema.Array(GraphQLError), null), + errors: Schema.OptionFromNullishOr(Schema.Array(GraphQLError), { onNoneEncoding: null }), }) const ViewerResponse = Schema.Struct({ - data: Schema.optionalWith( + data: Schema.OptionFromOptional( Schema.Struct({ viewer: Schema.Struct({ id: Schema.String, @@ -342,9 +341,8 @@ const ViewerResponse = Schema.Struct({ ), }), }), - { as: "Option" }, ), - errors: Schema.optionalWith(Schema.Array(GraphQLError), { as: "Option" }), + errors: Schema.OptionFromOptional(Schema.Array(GraphQLError)), }) // ============================================================================ @@ -484,22 +482,16 @@ export class LinearApiClient extends ServiceMap.Service()("Line return yield* response.json as Effect.Effect }).pipe( // Map HTTP client errors to LinearApiError - Effect.catchTag("TimeoutException", () => + Effect.catchTag("TimeoutError", () => Effect.fail(new LinearApiError({ message: "Request timed out" })), ), - Effect.catchTag("RequestError", (error) => - Effect.fail( - new LinearApiError({ - message: `Network error: ${String(error)}`, - cause: error, - }), - ), - ), - Effect.catchTag("ResponseError", (error) => + Effect.catchTag("HttpClientError", (error) => Effect.fail( new LinearApiError({ - message: `Response error: ${String(error)}`, - status: error.response.status, + message: error.response + ? `Response error: ${String(error)}` + : `Network error: ${String(error)}`, + status: error.response?.status, cause: error, }), ), @@ -514,7 +506,7 @@ export class LinearApiClient extends ServiceMap.Service()("Line const client = makeAuthenticatedClient(accessToken) const rawResponse = yield* executeGraphQL(client, GET_DEFAULT_TEAM_QUERY) - const response = yield* Schema.decodeUnknown(GetDefaultTeamResponse)(rawResponse).pipe( + const response = yield* Schema.decodeUnknownEffect(GetDefaultTeamResponse)(rawResponse).pipe( Effect.mapError( (parseError) => new LinearApiError({ @@ -571,7 +563,7 @@ export class LinearApiClient extends ServiceMap.Service()("Line description: params.description || null, }) - const response = yield* Schema.decodeUnknown(CreateIssueResponse)(rawResponse).pipe( + const response = yield* Schema.decodeUnknownEffect(CreateIssueResponse)(rawResponse).pipe( Effect.mapError( (parseError) => new LinearApiError({ @@ -626,7 +618,7 @@ export class LinearApiClient extends ServiceMap.Service()("Line Effect.annotateLogs("rawResponse", JSON.stringify(rawResponse, null, 2)), ) - const response = yield* Schema.decodeUnknown(GetIssueResponse)(rawResponse).pipe( + const response = yield* Schema.decodeUnknownEffect(GetIssueResponse)(rawResponse).pipe( Effect.tapError((parseError) => Effect.logError("Linear API parse error").pipe( Effect.annotateLogs("parseError", String(parseError)), @@ -696,7 +688,7 @@ export class LinearApiClient extends ServiceMap.Service()("Line const client = makeAuthenticatedClient(accessToken) const rawResponse = yield* executeGraphQL(client, VIEWER_QUERY) - const response = yield* Schema.decodeUnknown(ViewerResponse)(rawResponse).pipe( + const response = yield* Schema.decodeUnknownEffect(ViewerResponse)(rawResponse).pipe( Effect.mapError( (parseError) => new LinearApiError({ @@ -743,7 +735,5 @@ export class LinearApiClient extends ServiceMap.Service()("Line } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(FetchHttpClient.layer), - ) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(FetchHttpClient.layer)) } diff --git a/packages/rivet-effect/src/actor.ts b/packages/rivet-effect/src/actor.ts index 2c3653063..acdbc20d5 100644 --- a/packages/rivet-effect/src/actor.ts +++ b/packages/rivet-effect/src/actor.ts @@ -12,9 +12,9 @@ type AnyActorContext = ActorContext * externally-provided runtime resource injected by the Rivet framework, * not a service we construct via layers. */ -export class RivetActorContext extends ServiceMap.Service()("@hazel/rivet-effect/RivetActorContext") {} +export class RivetActorContext extends ServiceMap.Service()( + "@hazel/rivet-effect/RivetActorContext", +) {} export const provideActorContext = ( make: Effect.Effect, diff --git a/packages/schema/src/avatar-url.ts b/packages/schema/src/avatar-url.ts index dce3c552c..0568ca577 100644 --- a/packages/schema/src/avatar-url.ts +++ b/packages/schema/src/avatar-url.ts @@ -19,25 +19,21 @@ export const validateImageUrl = Effect.fn("validateImageUrl")(function* (url: st .head(url) .pipe(Effect.scoped, Effect.timeout(Duration.seconds(5))) .pipe( - Effect.catchTag( - "TimeoutError", - () => - Effect.fail( - new InvalidAvatarUrlError({ - message: "Avatar URL took too long to respond", - url, - }), - ), + Effect.catchTag("TimeoutError", () => + Effect.fail( + new InvalidAvatarUrlError({ + message: "Avatar URL took too long to respond", + url, + }), + ), ), - Effect.catchTag( - "HttpClientError", - (e) => - Effect.fail( - new InvalidAvatarUrlError({ - message: `Avatar URL request failed: ${e.message}`, - url, - }), - ), + Effect.catchTag("HttpClientError", (e) => + Effect.fail( + new InvalidAvatarUrlError({ + message: `Avatar URL request failed: ${e.message}`, + url, + }), + ), ), ) @@ -50,10 +46,10 @@ export const validateImageUrl = Effect.fn("validateImageUrl")(function* (url: st ) } - const contentType = Option.fromNullOr(response.headers["content-type"]) + const contentType = Option.fromNullishOr(response.headers["content-type"]) const isImage = Option.match(contentType, { onNone: () => false, - onSome: (ct) => ct.startsWith("image/"), + onSome: (ct: string) => ct.startsWith("image/"), }) if (!isImage) { @@ -66,10 +62,11 @@ export const validateImageUrl = Effect.fn("validateImageUrl")(function* (url: st } }) -export const AvatarUrl = Schema.String - .check(Schema.isPattern(/^https?:\/\/.+/i, { +export const AvatarUrl = Schema.String.check( + Schema.isPattern(/^https?:\/\/.+/i, { message: "Avatar URL must be a valid URL", - })) + }), +) .check(Schema.isMaxLength(2048)) .pipe( Schema.decode({ diff --git a/packages/schema/src/ids.ts b/packages/schema/src/ids.ts index 21a93e673..a8fcc0b00 100644 --- a/packages/schema/src/ids.ts +++ b/packages/schema/src/ids.ts @@ -1,39 +1,43 @@ import { Schema } from "effect" -export const ChannelId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/ChannelId")).annotate({ - description: "The ID of the channel where the message is posted", - title: "Channel ID", -}) +export const ChannelId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/ChannelId")) + .annotate({ + description: "The ID of the channel where the message is posted", + title: "Channel ID", + }) export type ChannelId = Schema.Schema.Type -export const ConnectConversationId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand("@HazelChat/ConnectConversationId"), -).annotate({ - description: "The ID of a Hazel Connect conversation", - title: "Connect Conversation ID", -}) +export const ConnectConversationId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/ConnectConversationId")) + .annotate({ + description: "The ID of a Hazel Connect conversation", + title: "Connect Conversation ID", + }) export type ConnectConversationId = Schema.Schema.Type -export const ConnectConversationChannelId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand("@HazelChat/ConnectConversationChannelId"), -).annotate({ - description: "The ID of a Hazel Connect conversation channel mount", - title: "Connect Conversation Channel ID", -}) +export const ConnectConversationChannelId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/ConnectConversationChannelId")) + .annotate({ + description: "The ID of a Hazel Connect conversation channel mount", + title: "Connect Conversation Channel ID", + }) export type ConnectConversationChannelId = Schema.Schema.Type -export const ConnectInviteId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/ConnectInviteId")).annotate({ - description: "The ID of a Hazel Connect invite", - title: "Connect Invite ID", -}) +export const ConnectInviteId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/ConnectInviteId")) + .annotate({ + description: "The ID of a Hazel Connect invite", + title: "Connect Invite ID", + }) export type ConnectInviteId = Schema.Schema.Type -export const ConnectParticipantId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand("@HazelChat/ConnectParticipantId"), -).annotate({ - description: "The ID of a Hazel Connect participant projection", - title: "Connect Participant ID", -}) +export const ConnectParticipantId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/ConnectParticipantId")) + .annotate({ + description: "The ID of a Hazel Connect participant projection", + title: "Connect Participant ID", + }) export type ConnectParticipantId = Schema.Schema.Type export const UserId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/UserId")).annotate({ @@ -48,120 +52,134 @@ export const BotId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@Ha }) export type BotId = Schema.Schema.Type -export const MessageId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/MessageId")).annotate({ - description: "The ID of the message being replied to", - title: "Reply To Message ID", -}) +export const MessageId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/MessageId")) + .annotate({ + description: "The ID of the message being replied to", + title: "Reply To Message ID", + }) export type MessageId = Schema.Schema.Type -export const MessageReactionId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/MessageReactionId")).annotate({ - description: "The ID of the message reaction", - title: "Message Reaction ID", -}) +export const MessageReactionId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/MessageReactionId")) + .annotate({ + description: "The ID of the message reaction", + title: "Message Reaction ID", + }) export type MessageReactionId = Schema.Schema.Type -export const MessageAttachmentId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand("@HazelChat/MessageAttachmentId"), -).annotate({ - description: "The ID of the message attachment", - title: "Message Attachment ID", -}) +export const MessageAttachmentId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/MessageAttachmentId")) + .annotate({ + description: "The ID of the message attachment", + title: "Message Attachment ID", + }) export type MessageAttachmentId = Schema.Schema.Type -export const AttachmentId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/AttachmentId")).annotate({ - description: "The ID of the attachment being replied to", - title: "Attachment ID", -}) +export const AttachmentId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/AttachmentId")) + .annotate({ + description: "The ID of the attachment being replied to", + title: "Attachment ID", + }) export type AttachmentId = Schema.Schema.Type -export const OrganizationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/OrganizationId")).annotate({ - description: "The ID of the organization", - title: "Organization ID", -}) +export const OrganizationId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/OrganizationId")) + .annotate({ + description: "The ID of the organization", + title: "Organization ID", + }) export type OrganizationId = Schema.Schema.Type -export const InvitationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/InvitationId")).annotate({ - description: "The ID of the invitation", - title: "Invitation ID", -}) +export const InvitationId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/InvitationId")) + .annotate({ + description: "The ID of the invitation", + title: "Invitation ID", + }) export type InvitationId = Schema.Schema.Type -export const PinnedMessageId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/PinnedMessageId")).annotate({ - description: "The ID of the pinned message", - title: "Pinned Message ID", -}) +export const PinnedMessageId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/PinnedMessageId")) + .annotate({ + description: "The ID of the pinned message", + title: "Pinned Message ID", + }) export type PinnedMessageId = Schema.Schema.Type -export const NotificationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/NotificationId")).annotate({ - description: "The ID of the notification", - title: "Notification ID", -}) +export const NotificationId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/NotificationId")) + .annotate({ + description: "The ID of the notification", + title: "Notification ID", + }) export type NotificationId = Schema.Schema.Type -export const ChannelMemberId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/ChannelMemberId")).annotate({ - description: "The ID of the channel member", - title: "Channel Member ID", -}) +export const ChannelMemberId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/ChannelMemberId")) + .annotate({ + description: "The ID of the channel member", + title: "Channel Member ID", + }) export type ChannelMemberId = Schema.Schema.Type -export const OrganizationMemberId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand("@HazelChat/OrganizationMemberId"), -).annotate({ - description: "The ID of the organization member", - title: "Organization Member ID", -}) +export const OrganizationMemberId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/OrganizationMemberId")) + .annotate({ + description: "The ID of the organization member", + title: "Organization Member ID", + }) export type OrganizationMemberId = Schema.Schema.Type -export const TypingIndicatorId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/TypingIndicatorId")).annotate({ - description: "The ID of the typing indicator", - title: "Typing Indicator ID", -}) +export const TypingIndicatorId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/TypingIndicatorId")) + .annotate({ + description: "The ID of the typing indicator", + title: "Typing Indicator ID", + }) export type TypingIndicatorId = Schema.Schema.Type -export const UserPresenceStatusId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand("@HazelChat/UserPresenceStatusId"), -).annotate({ - description: "The ID of the user presence status", - title: "User Presence Status ID", -}) +export const UserPresenceStatusId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/UserPresenceStatusId")) + .annotate({ + description: "The ID of the user presence status", + title: "User Presence Status ID", + }) export type UserPresenceStatusId = Schema.Schema.Type -export const IntegrationConnectionId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand("@HazelChat/IntegrationConnectionId"), -).annotate({ - description: "The ID of an integration connection", - title: "Integration Connection ID", -}) +export const IntegrationConnectionId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/IntegrationConnectionId")) + .annotate({ + description: "The ID of an integration connection", + title: "Integration Connection ID", + }) export type IntegrationConnectionId = Schema.Schema.Type -export const SyncConnectionId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/SyncConnectionId")).annotate({ - description: "The ID of a chat sync connection", - title: "Sync Connection ID", -}) +export const SyncConnectionId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/SyncConnectionId")) + .annotate({ + description: "The ID of a chat sync connection", + title: "Sync Connection ID", + }) export type SyncConnectionId = Schema.Schema.Type -export const ExternalChannelId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalChannelId")).annotate( - { - description: "The external channel identifier from a synced provider", - title: "External Channel ID", - }, -) +export const ExternalChannelId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalChannelId")).annotate({ + description: "The external channel identifier from a synced provider", + title: "External Channel ID", +}) export type ExternalChannelId = Schema.Schema.Type -export const ExternalMessageId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalMessageId")).annotate( - { - description: "The external message identifier from a synced provider", - title: "External Message ID", - }, -) +export const ExternalMessageId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalMessageId")).annotate({ + description: "The external message identifier from a synced provider", + title: "External Message ID", +}) export type ExternalMessageId = Schema.Schema.Type -export const ExternalWebhookId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalWebhookId")).annotate( - { - description: "The external webhook identifier from a synced provider", - title: "External Webhook ID", - }, -) +export const ExternalWebhookId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalWebhookId")).annotate({ + description: "The external webhook identifier from a synced provider", + title: "External Webhook ID", +}) export type ExternalWebhookId = Schema.Schema.Type export const ExternalUserId = Schema.String.pipe(Schema.brand("@HazelChat/ExternalUserId")).annotate({ @@ -176,46 +194,52 @@ export const ExternalThreadId = Schema.String.pipe(Schema.brand("@HazelChat/Exte }) export type ExternalThreadId = Schema.Schema.Type -export const SyncChannelLinkId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/SyncChannelLinkId")).annotate({ - description: "The ID of a chat sync channel link", - title: "Sync Channel Link ID", -}) +export const SyncChannelLinkId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/SyncChannelLinkId")) + .annotate({ + description: "The ID of a chat sync channel link", + title: "Sync Channel Link ID", + }) export type SyncChannelLinkId = Schema.Schema.Type -export const SyncMessageLinkId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/SyncMessageLinkId")).annotate({ - description: "The ID of a chat sync message link", - title: "Sync Message Link ID", -}) +export const SyncMessageLinkId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/SyncMessageLinkId")) + .annotate({ + description: "The ID of a chat sync message link", + title: "Sync Message Link ID", + }) export type SyncMessageLinkId = Schema.Schema.Type -export const SyncEventReceiptId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/SyncEventReceiptId")).annotate( - { +export const SyncEventReceiptId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/SyncEventReceiptId")) + .annotate({ description: "The ID of a chat sync event receipt", title: "Sync Event Receipt ID", - }, -) + }) export type SyncEventReceiptId = Schema.Schema.Type -export const IntegrationTokenId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/IntegrationTokenId")).annotate( - { +export const IntegrationTokenId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/IntegrationTokenId")) + .annotate({ description: "The ID of an integration token record", title: "Integration Token ID", - }, -) + }) export type IntegrationTokenId = Schema.Schema.Type -export const MessageIntegrationLinkId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand("@HazelChat/MessageIntegrationLinkId"), -).annotate({ - description: "The ID of a message-integration link", - title: "Message Integration Link ID", -}) +export const MessageIntegrationLinkId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/MessageIntegrationLinkId")) + .annotate({ + description: "The ID of a message-integration link", + title: "Message Integration Link ID", + }) export type MessageIntegrationLinkId = Schema.Schema.Type -export const ChannelWebhookId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/ChannelWebhookId")).annotate({ - description: "The ID of a channel webhook", - title: "Channel Webhook ID", -}) +export const ChannelWebhookId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/ChannelWebhookId")) + .annotate({ + description: "The ID of a channel webhook", + title: "Channel Webhook ID", + }) export type ChannelWebhookId = Schema.Schema.Type export const ChannelIcon = Schema.String.pipe(Schema.brand("@HazelChat/ChannelIcon")).annotate({ @@ -224,56 +248,66 @@ export const ChannelIcon = Schema.String.pipe(Schema.brand("@HazelChat/ChannelIc }) export type ChannelIcon = Schema.Schema.Type -export const GitHubSubscriptionId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand("@HazelChat/GitHubSubscriptionId"), -).annotate({ - description: "The ID of a GitHub subscription", - title: "GitHub Subscription ID", -}) +export const GitHubSubscriptionId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/GitHubSubscriptionId")) + .annotate({ + description: "The ID of a GitHub subscription", + title: "GitHub Subscription ID", + }) export type GitHubSubscriptionId = Schema.Schema.Type -export const BotCommandId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/BotCommandId")).annotate({ - description: "The ID of a bot command", - title: "Bot Command ID", -}) +export const BotCommandId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/BotCommandId")) + .annotate({ + description: "The ID of a bot command", + title: "Bot Command ID", + }) export type BotCommandId = Schema.Schema.Type -export const BotInstallationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/BotInstallationId")).annotate({ - description: "The ID of a bot installation", - title: "Bot Installation ID", -}) +export const BotInstallationId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/BotInstallationId")) + .annotate({ + description: "The ID of a bot installation", + title: "Bot Installation ID", + }) export type BotInstallationId = Schema.Schema.Type -export const ChannelSectionId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/ChannelSectionId")).annotate({ - description: "The ID of a channel section", - title: "Channel Section ID", -}) +export const ChannelSectionId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/ChannelSectionId")) + .annotate({ + description: "The ID of a channel section", + title: "Channel Section ID", + }) export type ChannelSectionId = Schema.Schema.Type -export const RssSubscriptionId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/RssSubscriptionId")).annotate({ - description: "The ID of an RSS subscription", - title: "RSS Subscription ID", -}) +export const RssSubscriptionId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/RssSubscriptionId")) + .annotate({ + description: "The ID of an RSS subscription", + title: "RSS Subscription ID", + }) export type RssSubscriptionId = Schema.Schema.Type -export const IntegrationRequestId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand("@HazelChat/IntegrationRequestId"), -).annotate({ - description: "The ID of an integration request", - title: "Integration Request ID", -}) +export const IntegrationRequestId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/IntegrationRequestId")) + .annotate({ + description: "The ID of an integration request", + title: "Integration Request ID", + }) export type IntegrationRequestId = Schema.Schema.Type -export const CustomEmojiId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("@HazelChat/CustomEmojiId")).annotate({ - description: "The ID of a custom emoji", - title: "Custom Emoji ID", -}) +export const CustomEmojiId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/CustomEmojiId")) + .annotate({ + description: "The ID of a custom emoji", + title: "Custom Emoji ID", + }) export type CustomEmojiId = Schema.Schema.Type -export const MessageOutboxEventId = Schema.String.check(Schema.isUUID()).pipe( - Schema.brand("@HazelChat/MessageOutboxEventId"), -).annotate({ - description: "The ID of a message outbox event", - title: "Message Outbox Event ID", -}) +export const MessageOutboxEventId = Schema.String.check(Schema.isUUID()) + .pipe(Schema.brand("@HazelChat/MessageOutboxEventId")) + .annotate({ + description: "The ID of a message outbox event", + title: "Message Outbox Event ID", + }) export type MessageOutboxEventId = Schema.Schema.Type diff --git a/packages/schema/src/workos.ts b/packages/schema/src/workos.ts index 5fee46513..5e597f708 100644 --- a/packages/schema/src/workos.ts +++ b/packages/schema/src/workos.ts @@ -1,43 +1,43 @@ import { Schema } from "effect" -export const WorkOSUserId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( - Schema.brand("@HazelChat/WorkOSUserId"), -).annotate({ - description: "A WorkOS user identifier", - title: "WorkOS User ID", -}) +export const WorkOSUserId = Schema.Trimmed.check(Schema.isNonEmpty()) + .pipe(Schema.brand("@HazelChat/WorkOSUserId")) + .annotate({ + description: "A WorkOS user identifier", + title: "WorkOS User ID", + }) export type WorkOSUserId = Schema.Schema.Type -export const WorkOSOrganizationId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( - Schema.brand("@HazelChat/WorkOSOrganizationId"), -).annotate({ - description: "A WorkOS organization identifier", - title: "WorkOS Organization ID", -}) +export const WorkOSOrganizationId = Schema.Trimmed.check(Schema.isNonEmpty()) + .pipe(Schema.brand("@HazelChat/WorkOSOrganizationId")) + .annotate({ + description: "A WorkOS organization identifier", + title: "WorkOS Organization ID", + }) export type WorkOSOrganizationId = Schema.Schema.Type -export const WorkOSSessionId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( - Schema.brand("@HazelChat/WorkOSSessionId"), -).annotate({ - description: "A WorkOS session identifier", - title: "WorkOS Session ID", -}) +export const WorkOSSessionId = Schema.Trimmed.check(Schema.isNonEmpty()) + .pipe(Schema.brand("@HazelChat/WorkOSSessionId")) + .annotate({ + description: "A WorkOS session identifier", + title: "WorkOS Session ID", + }) export type WorkOSSessionId = Schema.Schema.Type -export const WorkOSInvitationId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( - Schema.brand("@HazelChat/WorkOSInvitationId"), -).annotate({ - description: "A WorkOS invitation identifier", - title: "WorkOS Invitation ID", -}) +export const WorkOSInvitationId = Schema.Trimmed.check(Schema.isNonEmpty()) + .pipe(Schema.brand("@HazelChat/WorkOSInvitationId")) + .annotate({ + description: "A WorkOS invitation identifier", + title: "WorkOS Invitation ID", + }) export type WorkOSInvitationId = Schema.Schema.Type -export const WorkOSClientId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( - Schema.brand("@HazelChat/WorkOSClientId"), -).annotate({ - description: "A WorkOS client identifier", - title: "WorkOS Client ID", -}) +export const WorkOSClientId = Schema.Trimmed.check(Schema.isNonEmpty()) + .pipe(Schema.brand("@HazelChat/WorkOSClientId")) + .annotate({ + description: "A WorkOS client identifier", + title: "WorkOS Client ID", + }) export type WorkOSClientId = Schema.Schema.Type export const WorkOSRole = Schema.Literals(["admin", "member", "owner"]) From f5eef170e9575f3fb9b601e75cfa33491afadd49 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 09:20:39 +0100 Subject: [PATCH 12/34] fix errors --- .../src/cache/access-context-cache.ts | 28 ++-- .../src/cache/access-context-service.ts | 145 +++++++++--------- .../src/cache/redis-persistence.ts | 2 +- apps/electric-proxy/src/config.ts | 8 +- apps/electric-proxy/src/index.ts | 17 +- .../src/observability/metrics.ts | 12 +- .../src/proxy/electric-client.ts | 5 +- libs/bot-sdk/src/auth.ts | 6 +- libs/bot-sdk/src/bot-config.ts | 20 +-- libs/bot-sdk/src/command.ts | 2 +- libs/bot-sdk/src/gateway.ts | 6 +- libs/bot-sdk/src/hazel-bot-sdk.ts | 28 ++-- libs/bot-sdk/src/log-config.ts | 25 +-- 13 files changed, 150 insertions(+), 154 deletions(-) diff --git a/apps/electric-proxy/src/cache/access-context-cache.ts b/apps/electric-proxy/src/cache/access-context-cache.ts index 848d44b43..06b152e78 100644 --- a/apps/electric-proxy/src/cache/access-context-cache.ts +++ b/apps/electric-proxy/src/cache/access-context-cache.ts @@ -1,5 +1,6 @@ import type { ChannelId } from "@hazel/schema" -import { Duration, PrimaryKey, Schema } from "effect" +import { Duration, Schema } from "effect" +import { Persistable } from "effect/unstable/persistence" /** * Cache configuration constants @@ -35,20 +36,15 @@ export class AccessContextLookupError extends Schema.TaggedErrorClass()( - "BotAccessContextRequest", - { - failure: AccessContextLookupError, - success: BotAccessContextSchema, - payload: { - botId: Schema.String, - userId: Schema.String, - }, - }, -) { - [PrimaryKey.symbol]() { - return `bot:${this.botId}` +export class BotAccessContextRequest extends Persistable.Class<{ + payload: { + botId: string + userId: string } -} +}>()("BotAccessContextRequest", { + primaryKey: (payload) => `bot:${payload.botId}`, + success: BotAccessContextSchema, + error: AccessContextLookupError, +}) {} diff --git a/apps/electric-proxy/src/cache/access-context-service.ts b/apps/electric-proxy/src/cache/access-context-service.ts index c1a6ab5e7..222120c1e 100644 --- a/apps/electric-proxy/src/cache/access-context-service.ts +++ b/apps/electric-proxy/src/cache/access-context-service.ts @@ -1,7 +1,7 @@ -import { PersistedCache, type Persistence } from "effect/unstable/persistence" +import { PersistedCache, Persistence } from "effect/unstable/persistence" import { and, Database, eq, isNull, schema } from "@hazel/db" import type { BotId, ChannelId, UserId } from "@hazel/schema" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" import { AccessContextLookupError, type BotAccessContext, @@ -32,88 +32,91 @@ export interface AccessContextCache { * Note: Database.Database is intentionally NOT included in dependencies * as it's a global infrastructure layer provided at the application root. */ -export class AccessContextCacheService extends ServiceMap.Service()( +export class AccessContextCacheService extends ServiceMap.Service()( "AccessContextCacheService", - { - scoped: Effect.gen(function* () { - const db = yield* Database.Database +) { + static readonly make = Effect.gen(function* () { + const db = yield* Database.Database - // Create bot access context cache - const botCache = yield* PersistedCache.make({ - storeId: `${CACHE_STORE_ID}:bot`, + // Create bot access context cache + const botCache = yield* PersistedCache.make({ + storeId: `${CACHE_STORE_ID}:bot`, - lookup: (request: BotAccessContextRequest) => - Effect.gen(function* () { - yield* Effect.annotateCurrentSpan("cache.lookup_performed", true) - yield* Effect.annotateCurrentSpan("cache.result", "miss") - const botId = request.botId as BotId + lookup: (request: BotAccessContextRequest) => + Effect.gen(function* () { + yield* Effect.annotateCurrentSpan("cache.lookup_performed", true) + yield* Effect.annotateCurrentSpan("cache.result", "miss") + const botId = request.botId as BotId - // Query channels in all orgs where the bot is installed. - // Bots are org-level (not channel members), so we join - // bot_installations → channels by organizationId. - const channels = yield* db - .execute((client) => - client - .selectDistinct({ channelId: schema.channelsTable.id }) - .from(schema.botInstallationsTable) - .innerJoin( - schema.channelsTable, - and( - eq( - schema.channelsTable.organizationId, - schema.botInstallationsTable.organizationId, - ), - isNull(schema.channelsTable.deletedAt), + // Query channels in all orgs where the bot is installed. + // Bots are org-level (not channel members), so we join + // bot_installations → channels by organizationId. + const channels = yield* db + .execute((client) => + client + .selectDistinct({ channelId: schema.channelsTable.id }) + .from(schema.botInstallationsTable) + .innerJoin( + schema.channelsTable, + and( + eq( + schema.channelsTable.organizationId, + schema.botInstallationsTable.organizationId, ), - ) - .where(eq(schema.botInstallationsTable.botId, botId)), - ) - .pipe( - Effect.catchTag( - "DatabaseError", - (error) => + isNull(schema.channelsTable.deletedAt), + ), + ) + .where(eq(schema.botInstallationsTable.botId, botId)), + ) + .pipe( + Effect.catchTag( + "DatabaseError", + (error) => + Effect.fail( new AccessContextLookupError({ message: "Failed to query bot's channels", detail: error.message, entityId: request.botId, entityType: "bot", }), - ), - ) + ), + ), + ) - const channelIds = channels.map((c) => c.channelId) - yield* Effect.annotateCurrentSpan("cache.result_size", channelIds.length) + const channelIds = channels.map((c: { channelId: ChannelId }) => c.channelId) + yield* Effect.annotateCurrentSpan("cache.result_size", channelIds.length) - return { channelIds } - }), + return { channelIds } + }), - timeToLive: () => CACHE_TTL, - inMemoryCapacity: IN_MEMORY_CAPACITY, - inMemoryTTL: IN_MEMORY_TTL, - }) + timeToLive: (_exit, _request) => CACHE_TTL, + inMemoryCapacity: IN_MEMORY_CAPACITY, + inMemoryTTL: (_exit, _request) => IN_MEMORY_TTL, + }) - return { - getBotContext: Effect.fn("AccessContextCache.getBotContext")(function* ( - botId: BotId, - userId: UserId, - ) { - yield* Effect.annotateCurrentSpan("cache.system", "redis") - yield* Effect.annotateCurrentSpan("cache.name", "electric-proxy:access-context:bot") - yield* Effect.annotateCurrentSpan("cache.operation", "get") - yield* Effect.annotateCurrentSpan("cache.lookup_performed", false) - yield* Effect.annotateCurrentSpan("cache.result", "hit") - const result = yield* botCache.get(new BotAccessContextRequest({ botId, userId })) - return { channelIds: result.channelIds as readonly ChannelId[] } - }), + return { + getBotContext: Effect.fn("AccessContextCache.getBotContext")(function* ( + botId: BotId, + userId: UserId, + ) { + yield* Effect.annotateCurrentSpan("cache.system", "redis") + yield* Effect.annotateCurrentSpan("cache.name", "electric-proxy:access-context:bot") + yield* Effect.annotateCurrentSpan("cache.operation", "get") + yield* Effect.annotateCurrentSpan("cache.lookup_performed", false) + yield* Effect.annotateCurrentSpan("cache.result", "hit") + const result = yield* botCache.get(new BotAccessContextRequest({ botId, userId })) + return { channelIds: result.channelIds as readonly ChannelId[] } + }), - invalidateBot: Effect.fn("AccessContextCache.invalidateBot")(function* (botId: BotId) { - yield* Effect.annotateCurrentSpan("cache.system", "redis") - yield* Effect.annotateCurrentSpan("cache.name", "electric-proxy:access-context:bot") - yield* Effect.annotateCurrentSpan("cache.operation", "invalidate") - // Note: We don't have userId here, but invalidation only uses the primary key (botId) - yield* botCache.invalidate(new BotAccessContextRequest({ botId, userId: "" as UserId })) - }), - } satisfies AccessContextCache - }), - }, -) {} + invalidateBot: Effect.fn("AccessContextCache.invalidateBot")(function* (botId: BotId) { + yield* Effect.annotateCurrentSpan("cache.system", "redis") + yield* Effect.annotateCurrentSpan("cache.name", "electric-proxy:access-context:bot") + yield* Effect.annotateCurrentSpan("cache.operation", "invalidate") + // Note: We don't have userId here, but invalidation only uses the primary key (botId) + yield* botCache.invalidate(new BotAccessContextRequest({ botId, userId: "" as UserId })) + }), + } satisfies AccessContextCache + }) + + static readonly layer = Layer.effect(this, this.make) +} diff --git a/apps/electric-proxy/src/cache/redis-persistence.ts b/apps/electric-proxy/src/cache/redis-persistence.ts index 8413ea96a..19fa1ed19 100644 --- a/apps/electric-proxy/src/cache/redis-persistence.ts +++ b/apps/electric-proxy/src/cache/redis-persistence.ts @@ -7,7 +7,7 @@ import { ProxyConfigService } from "../config" * Redis persistence layer configured with proxy config. * Provides: Persistence.ResultPersistence */ -export const RedisPersistenceLive = Layer.unwrapEffect( +export const RedisPersistenceLive = Layer.unwrap( Effect.gen(function* () { const config = yield* ProxyConfigService yield* Effect.log("Connecting to Redis via @hazel/effect-bun", { url: config.redisUrl }) diff --git a/apps/electric-proxy/src/config.ts b/apps/electric-proxy/src/config.ts index 75d078399..11f13b17d 100644 --- a/apps/electric-proxy/src/config.ts +++ b/apps/electric-proxy/src/config.ts @@ -1,4 +1,4 @@ -import { ServiceMap, Config, Effect, Option, Redacted } from "effect" +import { ServiceMap, Config, Effect, Layer, Option, Redacted } from "effect" /** * Proxy configuration interface @@ -26,11 +26,11 @@ export class ProxyConfigService extends ServiceMap.Service() const electricUrl = yield* Config.string("ELECTRIC_URL") const electricSourceId = yield* Config.string("ELECTRIC_SOURCE_ID").pipe( Config.option, - Effect.map(Option.getOrUndefined), + Config.map(Option.getOrUndefined), ) const electricSourceSecret = yield* Config.string("ELECTRIC_SOURCE_SECRET").pipe( Config.option, - Effect.map(Option.getOrUndefined), + Config.map(Option.getOrUndefined), ) const workosApiKey = yield* Config.string("WORKOS_API_KEY") const workosClientId = yield* Config.string("WORKOS_CLIENT_ID") @@ -42,7 +42,7 @@ export class ProxyConfigService extends ServiceMap.Service() const port = yield* Config.number("PORT").pipe(Config.withDefault(8184)) const otlpEndpoint = yield* Config.string("OTLP_ENDPOINT").pipe( Config.option, - Effect.map(Option.getOrUndefined), + Config.map(Option.getOrUndefined), ) const redisUrl = yield* Config.redacted("REDIS_URL").pipe( Config.withDefault(Redacted.make("redis://localhost:6380")), diff --git a/apps/electric-proxy/src/index.ts b/apps/electric-proxy/src/index.ts index a8cf57eee..55d35b8cf 100644 --- a/apps/electric-proxy/src/index.ts +++ b/apps/electric-proxy/src/index.ts @@ -137,8 +137,9 @@ const handleUserRequest = (request: Request) => { Effect.gen(function* () { yield* annotateHandledError(401, "ProxyAuthenticationError") yield* Effect.logInfo("Authentication failed", { detail: error.detail }) - yield* Metric.increment(proxyAuthFailures).pipe( - Effect.tagMetrics({ auth_type: "user", error_tag: "ProxyAuthenticationError" }), + yield* Metric.update( + Metric.withAttributes(proxyAuthFailures, { auth_type: "user", error_tag: "ProxyAuthenticationError" }), + 1, ) return new Response( JSON.stringify({ @@ -180,7 +181,7 @@ const handleUserRequest = (request: Request) => { }), ), // Fallback for any unhandled errors - returns error details to client for debugging - Effect.catch((error) => + Effect.catchAll((error) => Effect.gen(function* () { const errorTag = (error as { _tag?: string })?._tag ?? "UnknownError" yield* annotateHandledError(500, errorTag) @@ -206,12 +207,13 @@ const handleUserRequest = (request: Request) => { const duration = Date.now() - start yield* Effect.annotateCurrentSpan("http.status_code", response.status) yield* Effect.annotateCurrentSpan("http.response.status_code", response.status) - yield* Metric.increment(proxyRequestsTotal).pipe( - Effect.tagMetrics({ + yield* Metric.update( + Metric.withAttributes(proxyRequestsTotal, { route: "/v1/shape", auth_type: "user", status_code: String(response.status), }), + 1, ) yield* Metric.update(proxyRequestDuration, duration) }), @@ -317,8 +319,9 @@ const handleBotRequest = (request: Request) => { yield* Effect.logInfo("Bot authentication failed", { detail: error.detail, }) - yield* Metric.increment(proxyAuthFailures).pipe( - Effect.tagMetrics({ auth_type: "bot", error_tag: "BotAuthenticationError" }), + yield* Metric.update( + Metric.withAttributes(proxyAuthFailures, { auth_type: "bot", error_tag: "BotAuthenticationError" }), + 1, ) return new Response( JSON.stringify({ diff --git a/apps/electric-proxy/src/observability/metrics.ts b/apps/electric-proxy/src/observability/metrics.ts index 108da3465..1a1016d6c 100644 --- a/apps/electric-proxy/src/observability/metrics.ts +++ b/apps/electric-proxy/src/observability/metrics.ts @@ -3,9 +3,9 @@ * * Provides counters and histograms for monitoring proxy performance. */ -import { Metric, MetricBoundaries } from "effect" +import { Metric } from "effect" -const latencyBoundaries = MetricBoundaries.fromIterable([5, 10, 25, 50, 100, 250, 500, 1000, 2500]) +const latencyBoundaries = [5, 10, 25, 50, 100, 250, 500, 1000, 2500] as const // ============================================================================ // Counters @@ -25,7 +25,11 @@ export const proxyElectricErrors = Metric.counter("proxy.electric.errors") // ============================================================================ /** End-to-end proxy request duration */ -export const proxyRequestDuration = Metric.histogram("proxy.request.duration_ms", latencyBoundaries) +export const proxyRequestDuration = Metric.histogram("proxy.request.duration_ms", { + boundaries: latencyBoundaries, +}) /** Upstream Electric fetch latency */ -export const proxyElectricDuration = Metric.histogram("proxy.electric.duration_ms", latencyBoundaries) +export const proxyElectricDuration = Metric.histogram("proxy.electric.duration_ms", { + boundaries: latencyBoundaries, +}) diff --git a/apps/electric-proxy/src/proxy/electric-client.ts b/apps/electric-proxy/src/proxy/electric-client.ts index d802984d5..c3e568416 100644 --- a/apps/electric-proxy/src/proxy/electric-client.ts +++ b/apps/electric-proxy/src/proxy/electric-client.ts @@ -83,8 +83,9 @@ export const proxyElectricRequest = Effect.fn("ElectricClient.proxyElectricReque if (upstreamRequestId) { yield* Effect.annotateCurrentSpan("electric.upstream_request_id", upstreamRequestId) } - yield* Metric.increment(proxyElectricErrors).pipe( - Effect.tagMetrics({ status_code: String(response.status) }), + yield* Metric.update( + Metric.withAttributes(proxyElectricErrors, { status_code: String(response.status) }), + 1, ) const errorBody = yield* Effect.promise(() => response.text()) yield* Effect.logWarning("Electric returned non-2xx", { diff --git a/libs/bot-sdk/src/auth.ts b/libs/bot-sdk/src/auth.ts index b76f6990c..8dd149431 100644 --- a/libs/bot-sdk/src/auth.ts +++ b/libs/bot-sdk/src/auth.ts @@ -1,7 +1,7 @@ import { HttpApiClient } from "effect/unstable/httpapi" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" import { HazelApi } from "@hazel/domain/http" -import { ServiceMap, Duration, Effect, Schedule } from "effect" +import { Layer, ServiceMap, Duration, Effect, Schedule } from "effect" import { AuthenticationError } from "./errors.ts" /** @@ -85,8 +85,8 @@ export const createAuthContextFromToken = ( Effect.retry( Schedule.exponential("1 second", 2).pipe( Schedule.jittered, - Schedule.whileOutput((duration) => - Duration.lessThanOrEqualTo(duration, Duration.seconds(30)), + Schedule.while((duration) => + Duration.isLessThanOrEqualTo(duration, Duration.seconds(30)), ), ), ), diff --git a/libs/bot-sdk/src/bot-config.ts b/libs/bot-sdk/src/bot-config.ts index c0952e6db..fa9d19e2d 100644 --- a/libs/bot-sdk/src/bot-config.ts +++ b/libs/bot-sdk/src/bot-config.ts @@ -18,24 +18,14 @@ const DEFAULT_ACTORS_URL = "https://rivet.hazel.sh" * - GATEWAY_URL (optional) - Gateway URL for inbound bot websocket delivery */ export const BotEnvConfig = Config.all({ - botToken: Config.redacted("BOT_TOKEN").pipe(Config.withDescription("Bot authentication token")), - backendUrl: Config.string("BACKEND_URL").pipe( - Config.withDefault("https://api.hazel.sh"), - Config.withDescription("Backend API URL"), - ), - gatewayUrl: Config.string("GATEWAY_URL").pipe( - Config.withDefault("https://bot-gateway.hazel.sh"), - Config.withDescription("Gateway API URL for inbound bot websocket delivery"), - ), + botToken: Config.redacted("BOT_TOKEN"), + backendUrl: Config.string("BACKEND_URL").pipe(Config.withDefault("https://api.hazel.sh")), + gatewayUrl: Config.string("GATEWAY_URL").pipe(Config.withDefault("https://bot-gateway.hazel.sh")), actorsUrl: Config.string("ACTORS_URL").pipe( Config.orElse(() => Config.string("RIVET_URL")), Config.withDefault(DEFAULT_ACTORS_URL), - Config.withDescription("Actors/Rivet endpoint for live state streaming"), - ), - healthPort: Config.number("PORT").pipe( - Config.withDefault(0), - Config.withDescription("Health check server port (default 0, OS-assigned)"), ), + healthPort: Config.number("PORT").pipe(Config.withDefault(0)), }) -export type BotEnvConfig = Config.Config.Success +export type BotEnvConfig = typeof BotEnvConfig extends Config ? T : never diff --git a/libs/bot-sdk/src/command.ts b/libs/bot-sdk/src/command.ts index e5380f5e6..54130aa06 100644 --- a/libs/bot-sdk/src/command.ts +++ b/libs/bot-sdk/src/command.ts @@ -93,7 +93,7 @@ export type CommandArgsFields = */ export type CommandArgs = G extends CommandGroup - ? Extract extends { argsSchema: Schema.Schema } + ? Extract extends { argsSchema: Schema.Schema } ? A : {} : never diff --git a/libs/bot-sdk/src/gateway.ts b/libs/bot-sdk/src/gateway.ts index ec6936761..0877663e1 100644 --- a/libs/bot-sdk/src/gateway.ts +++ b/libs/bot-sdk/src/gateway.ts @@ -1,5 +1,5 @@ import type { BotId } from "@hazel/schema" -import { Context, Effect, Layer, Ref } from "effect" +import { ServiceMap, Effect, Layer, Ref } from "effect" import { GatewaySessionStoreError } from "./errors.ts" export interface GatewaySessionStore { @@ -7,7 +7,7 @@ export interface GatewaySessionStore { save(botId: BotId, offset: string): Effect.Effect } -export const GatewaySessionStoreTag = Context.GenericTag( +export const GatewaySessionStoreTag = ServiceMap.Service( "@hazel/bot-sdk/GatewaySessionStore", ) @@ -53,7 +53,7 @@ export interface BotStateStore { delete(botId: BotId, key: string): Effect.Effect } -export const BotStateStoreTag = Context.GenericTag("@hazel/bot-sdk/BotStateStore") +export const BotStateStoreTag = ServiceMap.Service("@hazel/bot-sdk/BotStateStore") export const InMemoryBotStateStoreLive = Layer.effect( BotStateStoreTag, diff --git a/libs/bot-sdk/src/hazel-bot-sdk.ts b/libs/bot-sdk/src/hazel-bot-sdk.ts index 07fcafd5a..236bca1a4 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.ts @@ -45,11 +45,10 @@ import { LogLevel, ManagedRuntime, Option, - RateLimiter, Redacted, Ref, - Runtime, Schema, + Semaphore, ServiceMap, } from "effect" import { BotAuth, createAuthContextFromToken } from "./auth.ts" @@ -215,12 +214,11 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB botName: authContext.botName, } - // Create rate limiter for outbound message operations - // Default: 10 messages per second to prevent API rate limiting - const messageLimiter = yield* RateLimiter.make({ - limit: 10, - interval: Duration.seconds(1), - }) + // Create semaphore for outbound message operations + // Limit to 10 concurrent requests to prevent API rate limiting + const messageSemaphore = Semaphore.makeUnsafe(10) + const messageLimiter = (effect: Effect.Effect) => + Semaphore.withPermit(messageSemaphore)(effect) // Get the runtime config (optional - contains commands to sync) const runtimeConfigOption = yield* Effect.serviceOption(HazelBotRuntimeConfigTag) @@ -295,7 +293,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB }), }).pipe( Effect.flatMap((parsed) => - Schema.decodeUnknown(schema)(parsed).pipe( + Schema.decodeUnknownEffect(schema)(parsed).pipe( Effect.mapError( (cause) => new GatewayDecodeError({ @@ -379,7 +377,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB { onNone: () => Effect.succeed(event.payload.arguments), onSome: (def) => - Schema.decodeUnknown(def.argsSchema)(event.payload.arguments).pipe( + Schema.decodeUnknownEffect(def.argsSchema)(event.payload.arguments).pipe( Effect.mapError( (cause) => new CommandArgsDecodeError({ @@ -503,7 +501,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB partitionEvents, (envelope) => dispatchGatewayEvent(envelope).pipe( - Effect.tapErrorCause((cause) => + Effect.tapCause((cause) => Effect.logError("Gateway event handler failed", { eventType: envelope.eventType, partitionKey: envelope.partitionKey, @@ -683,7 +681,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB }, ), }), - Effect.zipRight( + Effect.andThen( Effect.logInfo( hasConnected || frame.resumed ? "Bot gateway websocket reconnected" @@ -814,7 +812,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB offset: nextResumeOffset, sessionId: nextSessionId, }).pipe( - Effect.zipRight(Effect.sleep(Duration.seconds(1))), + Effect.andThen(Effect.sleep(Duration.seconds(1))), Effect.as({ resumeOffset: nextResumeOffset, sessionId: nextSessionId, @@ -824,7 +822,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB ) nextResumeOffset = nextState.resumeOffset nextSessionId = nextState.sessionId - }).pipe(Effect.zipRight(Effect.sleep(Duration.millis(250)))), + }).pipe(Effect.andThen(Effect.sleep(Duration.millis(250)))), ), ) }) @@ -1403,7 +1401,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB }) .pipe( Effect.timeout(Duration.seconds(15)), - Effect.tapErrorCause((cause) => + Effect.tapCause((cause) => Effect.logError("[bot.channel.createThread] Failed to ensure thread", { messageId, channelId, diff --git a/libs/bot-sdk/src/log-config.ts b/libs/bot-sdk/src/log-config.ts index b78ace6ea..dca5ca654 100644 --- a/libs/bot-sdk/src/log-config.ts +++ b/libs/bot-sdk/src/log-config.ts @@ -69,7 +69,7 @@ export interface BotLogConfig { * Default log configuration */ export const defaultLogConfig: BotLogConfig = { - level: LogLevel.Info, + level: "Info", format: "pretty", } @@ -77,7 +77,7 @@ export const defaultLogConfig: BotLogConfig = { * Production log configuration */ export const productionLogConfig: BotLogConfig = { - level: LogLevel.Info, + level: "Info", format: "structured", } @@ -85,7 +85,7 @@ export const productionLogConfig: BotLogConfig = { * Debug log configuration (all DEBUG output) */ export const debugLogConfig: BotLogConfig = { - level: LogLevel.Debug, + level: "Debug", format: "pretty", } @@ -104,22 +104,23 @@ export const createLoggerLayer = (config: BotLogConfig): Layer.Layer => { export const logLevelFromString = (level: string): LogLevel.LogLevel => { switch (level.toLowerCase()) { case "all": - return LogLevel.All + return "All" case "trace": - return LogLevel.Trace + return "Trace" case "debug": - return LogLevel.Debug + return "Debug" case "info": - return LogLevel.Info + return "Info" case "warning": - return LogLevel.Warning + case "warn": + return "Warn" case "error": - return LogLevel.Error + return "Error" case "fatal": - return LogLevel.Fatal + return "Fatal" case "none": - return LogLevel.None + return "None" default: - return LogLevel.Info + return "Info" } } From 11aa2a12444c4c013abea30ee2418a25b9964b38 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 09:35:29 +0100 Subject: [PATCH 13/34] fix stuff --- apps/backend/fix-static-accessors.mjs | 141 +++++ apps/backend/fix-static-accessors2.mjs | 142 +++++ apps/backend/package.json | 4 +- apps/backend/src/http.ts | 4 +- apps/backend/src/index.ts | 16 +- apps/backend/src/lib/env-vars.ts | 1 + apps/backend/src/lib/policy-utils.ts | 15 +- apps/backend/src/lib/schema.ts | 8 +- .../src/policies/channel-section-policy.ts | 4 +- .../src/policies/channel-webhook-policy.ts | 4 +- .../policies/github-subscription-policy.ts | 4 +- .../policies/integration-connection-policy.ts | 4 +- .../src/policies/message-reaction-policy.ts | 4 +- .../policies/organization-member-policy.ts | 4 +- .../src/policies/rss-subscription-policy.ts | 4 +- .../src/policies/typing-indicator-policy.ts | 4 +- .../policies/user-presence-status-policy.ts | 4 +- .../src/routes/api-v1/messages.http.ts | 44 +- apps/backend/src/routes/bot-commands.http.ts | 4 +- apps/backend/src/routes/presence.http.ts | 3 +- apps/backend/src/routes/uploads.http.ts | 11 +- apps/backend/src/rpc/handlers/attachments.ts | 14 +- apps/backend/src/rpc/handlers/bots.ts | 30 +- .../src/rpc/handlers/channel-members.ts | 42 +- .../src/rpc/handlers/channel-sections.ts | 27 +- .../src/rpc/handlers/channel-webhooks.ts | 11 +- apps/backend/src/rpc/handlers/channels.ts | 78 +-- apps/backend/src/rpc/handlers/chat-sync.ts | 15 +- .../backend/src/rpc/handlers/custom-emojis.ts | 32 +- .../src/rpc/handlers/github-subscriptions.ts | 11 +- apps/backend/src/rpc/handlers/invitations.ts | 28 +- .../src/rpc/handlers/message-reactions.ts | 28 +- apps/backend/src/rpc/handlers/messages.ts | 22 +- .../backend/src/rpc/handlers/notifications.ts | 22 +- .../src/rpc/handlers/organization-members.ts | 25 +- .../backend/src/rpc/handlers/organizations.ts | 65 ++- .../src/rpc/handlers/pinned-messages.ts | 16 +- .../src/rpc/handlers/rss-subscriptions.ts | 11 +- .../src/rpc/handlers/typing-indicators.ts | 18 +- .../src/rpc/handlers/user-presence-status.ts | 22 +- apps/backend/src/rpc/handlers/users.ts | 26 +- .../src/rpc/middleware/scope-injection.ts | 4 +- .../src/services/channel-access-sync.ts | 8 +- .../chat-sync-attribution-reconciler.ts | 4 +- .../chat-sync/chat-sync-core-worker.ts | 75 +-- .../chat-sync/chat-sync-provider-registry.ts | 4 +- .../services/chat-sync/discord-sync-worker.ts | 18 +- .../services/connect-conversation-service.ts | 14 +- apps/backend/src/services/database.ts | 2 +- .../src/services/integration-token-service.ts | 4 +- .../src/services/message-outbox-dispatcher.ts | 4 +- .../services/message-side-effect-service.ts | 4 +- apps/backend/src/services/org-resolver.ts | 8 +- apps/bot-gateway/src/index.test.ts | 74 +-- apps/bot-gateway/src/index.ts | 60 ++- .../cluster/src/cron/presence-cleanup-cron.ts | 2 +- apps/cluster/src/cron/rss-poll-cron.ts | 2 +- .../src/cron/status-expiration-cron.ts | 2 +- .../src/cron/typing-indicator-cleanup-cron.ts | 2 +- apps/cluster/src/cron/upload-cleanup-cron.ts | 2 +- apps/cluster/src/cron/workos-sync-cron.ts | 5 +- apps/cluster/src/index.ts | 29 +- .../src/workflows/cleanup-uploads-handler.ts | 202 +++---- .../workflows/github-installation-handler.ts | 250 ++++----- .../src/workflows/github-webhook-handler.ts | 242 +++++---- .../workflows/message-notification-handler.ts | 508 +++++++++--------- .../src/workflows/rss-feed-poll-handler.ts | 362 +++++++------ .../src/workflows/thread-naming-handler.ts | 508 +++++++++--------- .../src/cache/access-context-service.ts | 160 +++--- apps/electric-proxy/src/index.ts | 28 +- apps/link-preview-worker/src/api.ts | 4 +- apps/link-preview-worker/src/declare.ts | 75 ++- apps/link-preview-worker/src/handle.ts | 6 +- .../src/handlers/link-preview.ts | 18 +- .../link-preview-worker/src/handlers/tweet.ts | 13 +- apps/link-preview-worker/src/index.ts | 48 +- apps/web/src/atoms/chat-atoms.ts | 2 +- apps/web/src/atoms/chat-query-atoms.ts | 2 +- apps/web/src/atoms/command-palette-state.ts | 2 +- apps/web/src/atoms/custom-emoji-atoms.ts | 4 +- apps/web/src/atoms/desktop-auth.ts | 35 +- apps/web/src/atoms/desktop-callback-atoms.ts | 4 +- apps/web/src/atoms/emoji-atoms.ts | 2 +- apps/web/src/atoms/feature-discovery-atoms.ts | 3 +- apps/web/src/atoms/hotkey-atoms.ts | 3 +- apps/web/src/atoms/loading-state-atoms.ts | 2 +- apps/web/src/atoms/message-atoms.ts | 4 +- apps/web/src/atoms/modal-atoms.ts | 3 +- .../web/src/atoms/notification-sound-atoms.ts | 2 +- apps/web/src/atoms/onboarding-atoms.ts | 2 +- apps/web/src/atoms/organization-atoms.ts | 2 +- apps/web/src/atoms/panel-atoms.ts | 3 +- apps/web/src/atoms/presence-atoms.ts | 2 +- apps/web/src/atoms/react-scan-atoms.ts | 2 +- apps/web/src/atoms/recent-channels-atom.ts | 2 +- apps/web/src/atoms/search-atoms.ts | 2 +- apps/web/src/atoms/section-collapse-atoms.ts | 2 +- apps/web/src/atoms/sidebar-atoms.ts | 2 +- apps/web/src/atoms/tauri-update-atoms.ts | 2 +- apps/web/src/atoms/web-auth.ts | 6 +- apps/web/src/atoms/web-callback-atoms.ts | 2 +- .../add-github-repo-modal.tsx | 3 +- .../chat-sync/add-channel-link-modal.tsx | 13 +- .../chat-sync/add-connection-modal.tsx | 13 +- .../components/chat/inline-thread-preview.tsx | 7 +- .../components/chat/message-reply-section.tsx | 3 +- apps/web/src/components/chat/message.tsx | 5 +- .../src/components/chat/reaction-button.tsx | 5 +- .../autocomplete/triggers/emoji-trigger.tsx | 5 +- .../chat/slate-editor/mention-element.tsx | 5 +- .../components/chat/thread-message-list.tsx | 5 +- .../components/chat/user-profile-popover.tsx | 5 +- .../emoji-picker/custom-emoji-section.tsx | 5 +- .../src/components/gif-picker/use-klipy.ts | 15 +- .../add-github-subscription-modal.tsx | 3 +- .../integrations/github-pr-embed.tsx | 3 +- .../integrations/linear-issue-embed.tsx | 3 +- apps/web/src/components/link-preview.tsx | 7 +- apps/web/src/components/theme-provider.tsx | 3 +- apps/web/src/components/tweet-embed.tsx | 7 +- apps/web/src/hooks/use-presence.ts | 23 +- apps/web/src/lib/auth.tsx | 7 +- .../lib/platform-storage/platform-runtime.ts | 4 +- apps/web/src/lib/registry.ts | 5 +- .../src/lib/services/common/atom-client.ts | 4 +- .../services/common/link-preview-client.ts | 4 +- .../lib/services/common/rpc-atom-client.ts | 4 +- apps/web/src/lib/services/common/runtime.ts | 2 +- .../lib/services/desktop/token-exchange.ts | 4 +- .../src/lib/services/desktop/token-storage.ts | 4 +- .../web/src/lib/services/web/token-storage.ts | 4 +- apps/web/src/providers/chat-provider.tsx | 5 +- .../channels/$channelId/settings/connect.tsx | 7 +- .../routes/_app/$orgSlug/profile/$userId.tsx | 5 +- .../_app/$orgSlug/settings/authentication.tsx | 13 +- .../settings/chat-sync/$connectionId.tsx | 15 +- .../$orgSlug/settings/chat-sync/index.tsx | 9 +- .../$orgSlug/settings/connect-invites.tsx | 7 +- .../settings/integrations/$integrationId.tsx | 9 +- apps/web/src/routes/join/$slug.tsx | 7 +- bots/hazel-bot/src/agent-loop.ts | 40 +- bots/hazel-bot/src/degeneration-detector.ts | 46 +- bots/hazel-bot/src/handler.ts | 41 +- bots/hazel-bot/src/index.ts | 34 +- bots/hazel-bot/src/tools/base.ts | 4 +- bots/hazel-bot/src/tools/craft.ts | 28 +- bots/hazel-bot/src/tools/linear.ts | 24 +- bots/hazel-bot/src/tools/toolkit.ts | 90 ++-- bots/linear-bot/src/index.ts | 32 +- bun.lock | 46 +- libs/bot-sdk/src/auth.ts | 6 +- libs/bot-sdk/src/bot-config.ts | 4 +- libs/bot-sdk/src/hazel-bot-sdk.ts | 106 ++-- libs/bot-sdk/src/log-config.ts | 9 +- libs/bot-sdk/src/log-context.ts | 18 +- libs/bot-sdk/src/retry.ts | 18 +- libs/bot-sdk/src/rpc/auth-middleware.ts | 4 +- libs/bot-sdk/src/rpc/client.ts | 2 +- libs/bot-sdk/src/run-bot.ts | 2 +- libs/bot-sdk/src/services/health-server.ts | 8 +- libs/bot-sdk/src/streaming/actors-client.ts | 4 +- libs/bot-sdk/src/streaming/types.ts | 2 +- .../src/service.ts | 14 +- packages/effect-bun/src/Telemetry.ts | 2 +- packages/rivet-effect/src/runtime.ts | 2 +- 165 files changed, 2544 insertions(+), 2119 deletions(-) create mode 100644 apps/backend/fix-static-accessors.mjs create mode 100644 apps/backend/fix-static-accessors2.mjs diff --git a/apps/backend/fix-static-accessors.mjs b/apps/backend/fix-static-accessors.mjs new file mode 100644 index 000000000..daf5fd4f9 --- /dev/null +++ b/apps/backend/fix-static-accessors.mjs @@ -0,0 +1,141 @@ +import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs'; +import { join, resolve } from 'path'; + +const serviceMap = { + 'AttachmentPolicy': 'attachmentPolicy', + 'MessagePolicy': 'messagePolicy', + 'ChannelPolicy': 'channelPolicy', + 'ChannelMemberPolicy': 'channelMemberPolicy', + 'ChannelSectionPolicy': 'channelSectionPolicy', + 'OrganizationPolicy': 'organizationPolicy', + 'OrganizationMemberPolicy': 'organizationMemberPolicy', + 'InvitationPolicy': 'invitationPolicy', + 'MessageReactionPolicy': 'messageReactionPolicy', + 'NotificationPolicy': 'notificationPolicy', + 'PinnedMessagePolicy': 'pinnedMessagePolicy', + 'TypingIndicatorPolicy': 'typingIndicatorPolicy', + 'UserPolicy': 'userPolicy', + 'UserPresenceStatusPolicy': 'userPresenceStatusPolicy', + 'IntegrationConnectionPolicy': 'integrationConnectionPolicy', + 'ChannelWebhookPolicy': 'channelWebhookPolicy', + 'GitHubSubscriptionPolicy': 'gitHubSubscriptionPolicy', + 'RssSubscriptionPolicy': 'rssSubscriptionPolicy', + 'BotPolicy': 'botPolicy', + 'CustomEmojiPolicy': 'customEmojiPolicy', + 'AttachmentRepo': 'attachmentRepo', + 'MessageRepo': 'messageRepo', + 'ChannelRepo': 'channelRepo', + 'ChannelMemberRepo': 'channelMemberRepo', + 'ChannelSectionRepo': 'channelSectionRepo', + 'OrganizationRepo': 'organizationRepo', + 'OrganizationMemberRepo': 'organizationMemberRepo', + 'InvitationRepo': 'invitationRepo', + 'UserRepo': 'userRepo', + 'PinnedMessageRepo': 'pinnedMessageRepo', + 'TypingIndicatorRepo': 'typingIndicatorRepo', + 'NotificationRepo': 'notificationRepo', + 'MessageReactionRepo': 'messageReactionRepo', + 'MessageOutboxRepo': 'messageOutboxRepo', + 'UserPresenceStatusRepo': 'userPresenceStatusRepo', + 'IntegrationConnectionRepo': 'integrationConnectionRepo', + 'IntegrationTokenRepo': 'integrationTokenRepo', + 'ChannelWebhookRepo': 'channelWebhookRepo', + 'GitHubSubscriptionRepo': 'gitHubSubscriptionRepo', + 'RssSubscriptionRepo': 'rssSubscriptionRepo', + 'BotRepo': 'botRepo', + 'BotCommandRepo': 'botCommandRepo', + 'BotInstallationRepo': 'botInstallationRepo', + 'CustomEmojiRepo': 'customEmojiRepo', + 'ConnectConversationRepo': 'connectConversationRepo', + 'ConnectConversationChannelRepo': 'connectConversationChannelRepo', + 'ConnectInviteRepo': 'connectInviteRepo', + 'ConnectParticipantRepo': 'connectParticipantRepo', + 'ChatSyncConnectionRepo': 'chatSyncConnectionRepo', + 'ChatSyncChannelLinkRepo': 'chatSyncChannelLinkRepo', + 'ChatSyncMessageLinkRepo': 'chatSyncMessageLinkRepo', + 'ChatSyncEventReceiptRepo': 'chatSyncEventReceiptRepo', + 'ChannelAccessSyncService': 'channelAccessSync', + 'DiscordSyncWorker': 'discordSyncWorker', + 'ConnectConversationService': 'connectConversationService', +}; + +// Skip these patterns (they're not static accessor calls) +const skipPatterns = ['layer', 'toLayer', 'Default', 'make', 'of(']; + +function processFile(filePath) { + let content = readFileSync(filePath, 'utf8'); + const originalContent = content; + + // Find all static accessor usages + const usedServices = new Set(); + for (const [className, varName] of Object.entries(serviceMap)) { + // Match ClassName.something where something starts with lowercase + // and is not a known non-accessor pattern + const regex = new RegExp(`(? router.add("GET", "/health", HttpServerResponse.text("OK"))) -const DocsRoute = HttpApiScalar.layerHttpRouter({ - api: HazelApi, +const DocsRoute = HttpApiScalar.layer(HazelApi, { path: "/docs", }) // HTTP RPC endpoint -const RpcRoute = RpcServer.layerHttpRouter({ +const RpcRoute = RpcServer.layerHttp({ group: AllRpcs, path: "/rpc", protocol: "http", @@ -216,12 +215,15 @@ const MainLive = Layer.mergeAll( SessionManager.layer, ).pipe( Layer.provideMerge(FetchHttpClient.layer), - Layer.provideMerge(Layer.setConfigProvider(ConfigProvider.fromEnv())), + Layer.provideMerge(ConfigProvider.layer(ConfigProvider.fromEnv())), ) const ServerLayer = HttpRouter.serve(AllRoutes).pipe( - HttpMiddleware.withTracerDisabledWhen( - (request) => request.url === "/health" || request.method === "OPTIONS", + Layer.provide( + Layer.succeed( + HttpMiddleware.TracerDisabledWhen, + (request: any) => request.url === "/health" || request.method === "OPTIONS", + ), ), Layer.provide(MainLive), Layer.provide(TracerLive), diff --git a/apps/backend/src/lib/env-vars.ts b/apps/backend/src/lib/env-vars.ts index a84e0691d..ba7099caf 100644 --- a/apps/backend/src/lib/env-vars.ts +++ b/apps/backend/src/lib/env-vars.ts @@ -1,5 +1,6 @@ import * as Config from "effect/Config" import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" import * as ServiceMap from "effect/ServiceMap" export class EnvVars extends ServiceMap.Service()("EnvVars", { diff --git a/apps/backend/src/lib/policy-utils.ts b/apps/backend/src/lib/policy-utils.ts index d23e21531..6ee70ad92 100644 --- a/apps/backend/src/lib/policy-utils.ts +++ b/apps/backend/src/lib/policy-utils.ts @@ -1,7 +1,7 @@ import { CurrentUser, ErrorUtils, policy } from "@hazel/domain" import type { ApiScope } from "@hazel/domain/scopes" import { CurrentRpcScopes } from "@hazel/domain/scopes" -import { Effect, FiberRef } from "effect" +import { Effect } from "effect" export type OrganizationRole = "admin" | "member" | "owner" @@ -28,7 +28,7 @@ export const withPolicyUnauthorized = ( entity: string, action: string, make: Effect.Effect, -) => ErrorUtils.refailUnauthorized(entity, action)(effect) +) => ErrorUtils.refailUnauthorized(entity, action)(make) /** * Reads the annotated scope from CurrentRpcScopes (injected by ScopeInjectionMiddleware) @@ -44,17 +44,18 @@ export const withPolicyUnauthorized = ( */ export const withAnnotatedScope = ( fn: (scope: ApiScope) => Effect.Effect, -): Effect.Effect => - Effect.flatMap(FiberRef.get(CurrentRpcScopes), (scopes) => { +): Effect.Effect => + Effect.gen(function* () { + const scopes = yield* CurrentRpcScopes if (scopes.length === 0) { - return Effect.die( + return yield* Effect.die( new Error("No RequiredScopes annotation on this RPC — cannot resolve annotated scope"), ) } if (scopes.length > 1) { - return Effect.die( + return yield* Effect.die( new Error(`withAnnotatedScope only supports single-scope RPCs; got [${scopes.join(", ")}]`), ) } - return fn(scopes[0]!) + return yield* fn(scopes[0]!) }) diff --git a/apps/backend/src/lib/schema.ts b/apps/backend/src/lib/schema.ts index c3dcaeb5c..b796c21de 100644 --- a/apps/backend/src/lib/schema.ts +++ b/apps/backend/src/lib/schema.ts @@ -1,11 +1,9 @@ import { Schema } from "effect" -export const RelativeUrl = Schema.String.pipe( +export const RelativeUrl = Schema.String.check( Schema.isNonEmpty(), - Schema.startsWith("/"), - Schema.filter((url) => !url.startsWith("//"), { - message: () => "Protocol-relative URLs are not allowed", - }), + Schema.isStartsWith("/"), + Schema.makeFilter((url: string) => !url.startsWith("//") || "Protocol-relative URLs are not allowed"), ) export const AuthState = Schema.Struct({ diff --git a/apps/backend/src/policies/channel-section-policy.ts b/apps/backend/src/policies/channel-section-policy.ts index 40bbfd921..1576716eb 100644 --- a/apps/backend/src/policies/channel-section-policy.ts +++ b/apps/backend/src/policies/channel-section-policy.ts @@ -8,7 +8,7 @@ import { OrgResolver } from "../services/org-resolver" export class ChannelSectionPolicy extends ServiceMap.Service()( "ChannelSectionPolicy/Policy", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const policyEntity = "ChannelSection" as const const orgResolver = yield* OrgResolver @@ -72,7 +72,7 @@ export class ChannelSectionPolicy extends ServiceMap.Service()( "ChannelWebhookPolicy/Policy", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const policyEntity = "ChannelWebhook" as const const channelRepo = yield* ChannelRepo @@ -88,7 +88,7 @@ export class ChannelWebhookPolicy extends ServiceMap.Service()( "GitHubSubscriptionPolicy/Policy", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const policyEntity = "GitHubSubscription" as const const channelRepo = yield* ChannelRepo @@ -98,7 +98,7 @@ export class GitHubSubscriptionPolicy extends ServiceMap.Service()( "IntegrationConnectionPolicy/Policy", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const policyEntity = "IntegrationConnection" as const const orgResolver = yield* OrgResolver @@ -56,5 +56,5 @@ export class IntegrationConnectionPolicy extends ServiceMap.Service()( "MessageReactionPolicy/Policy", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const policyEntity = "MessageReaction" as const const messageReactionRepo = yield* MessageReactionRepo @@ -101,7 +101,7 @@ export class MessageReactionPolicy extends ServiceMap.Service()( "OrganizationMemberPolicy/Policy", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const policyEntity = "OrganizationMember" as const const organizationMemberRepo = yield* OrganizationMemberRepo @@ -105,7 +105,7 @@ export class OrganizationMemberPolicy extends ServiceMap.Service()( "RssSubscriptionPolicy/Policy", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const policyEntity = "RssSubscription" as const const channelRepo = yield* ChannelRepo @@ -98,7 +98,7 @@ export class RssSubscriptionPolicy extends ServiceMap.Service()( "TypingIndicatorPolicy/Policy", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const policyEntity = "TypingIndicator" as const const authorize = makePolicy(policyEntity) @@ -54,7 +54,7 @@ export class TypingIndicatorPolicy extends ServiceMap.Service()( "UserPresenceStatusPolicy/Policy", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const policyEntity = "UserPresenceStatus" as const const authorize = makePolicy(policyEntity) @@ -20,5 +20,5 @@ export class UserPresenceStatusPolicy extends ServiceMap.Service { */ const authenticateBotFromToken = Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest + const attachmentPolicy = yield* AttachmentPolicy + const messagePolicy = yield* MessagePolicy + const messageReactionPolicy = yield* MessageReactionPolicy + const attachmentRepo = yield* AttachmentRepo + const messageRepo = yield* MessageRepo + const messageReactionRepo = yield* MessageReactionRepo const authHeader = request.headers.authorization if (!authHeader || !authHeader.startsWith("Bearer ")) { @@ -123,7 +129,7 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag const effectiveLimit = limit ?? 25 // First, check if user can read this channel (policy authorization) - yield* MessagePolicy.canRead(channel_id).pipe( + yield* messagePolicy.canRead(channel_id).pipe( Effect.provideService(CurrentUser.Context, currentUser), ) @@ -142,7 +148,7 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag | undefined = undefined if (starting_after) { - const cursorMsg = yield* MessageRepo.findByIdForCursor({ + const cursorMsg = yield* messageRepo.findByIdForCursor({ id: starting_after, channelId: channel_id, }) @@ -158,7 +164,7 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag createdAt: cursorMsg.value.createdAt, } } else if (ending_before) { - const cursorMsg = yield* MessageRepo.findByIdForCursor({ + const cursorMsg = yield* messageRepo.findByIdForCursor({ id: ending_before, channelId: channel_id, }) @@ -176,7 +182,7 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag } // Query messages (policy already checked, use system actor for db access) - const messages = yield* MessageRepo.listByChannel({ + const messages = yield* messageRepo.listByChannel({ channelId: channel_id, cursorBefore, cursorAfter, @@ -219,8 +225,8 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag const response = yield* db .transaction( Effect.gen(function* () { - yield* MessagePolicy.canCreate(rest.channelId) - const createdMessage = yield* MessageRepo.insert({ + yield* messagePolicy.canCreate(rest.channelId) + const createdMessage = yield* messageRepo.insert({ ...rest, embeds: embeds ?? null, replyToMessageId: replyToMessageId ?? null, @@ -233,8 +239,8 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag if (attachmentIds && attachmentIds.length > 0) { yield* Effect.forEach(attachmentIds, (attachmentId) => Effect.gen(function* () { - yield* AttachmentPolicy.canUpdate(attachmentId) - yield* AttachmentRepo.update({ + yield* attachmentPolicy.canUpdate(attachmentId) + yield* attachmentRepo.update({ id: attachmentId, messageId: createdMessage.id, }) @@ -306,8 +312,8 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag const response = yield* db .transaction( Effect.gen(function* () { - yield* MessagePolicy.canUpdate(path.id) - const updatedMessage = yield* MessageRepo.update({ + yield* messagePolicy.canUpdate(path.id) + const updatedMessage = yield* messageRepo.update({ id: path.id, ...rest, ...(embeds !== undefined ? { embeds } : {}), @@ -365,15 +371,15 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag Effect.gen(function* () { const bot = yield* authenticateBotFromToken const currentUser = createBotUserContext(bot) - const existingMessage = yield* MessageRepo.findById(path.id) + const existingMessage = yield* messageRepo.findById(path.id) yield* checkMessageRateLimit(bot.userId) const response = yield* db .transaction( Effect.gen(function* () { - yield* MessagePolicy.canDelete(path.id) - yield* MessageRepo.deleteById(path.id) + yield* messagePolicy.canDelete(path.id) + yield* messageRepo.deleteById(path.id) if (Option.isSome(existingMessage)) { yield* outboxRepo.insert({ @@ -441,9 +447,9 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag const { emoji, channelId } = payload const messageId = path.id - yield* MessageReactionPolicy.canList(messageId) + yield* messageReactionPolicy.canList(messageId) const existingReaction = - yield* MessageReactionRepo.findByMessageUserEmoji( + yield* messageReactionRepo.findByMessageUserEmoji( messageId, bot.userId, emoji, @@ -461,8 +467,8 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag userId: existingReaction.value.userId, } as const - yield* MessageReactionPolicy.canDelete(existingReaction.value.id) - yield* MessageReactionRepo.deleteById(existingReaction.value.id) + yield* messageReactionPolicy.canDelete(existingReaction.value.id) + yield* messageReactionRepo.deleteById(existingReaction.value.id) yield* outboxRepo.insert({ eventType: "reaction_deleted", aggregateId: existingReaction.value.id, @@ -484,8 +490,8 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag } // Otherwise, create a new reaction - yield* MessageReactionPolicy.canCreate(messageId) - const createdReaction = yield* MessageReactionRepo.insert({ + yield* messageReactionPolicy.canCreate(messageId) + const createdReaction = yield* messageReactionRepo.insert({ messageId, channelId, emoji, diff --git a/apps/backend/src/routes/bot-commands.http.ts b/apps/backend/src/routes/bot-commands.http.ts index 0a18addbf..56f4b3f75 100644 --- a/apps/backend/src/routes/bot-commands.http.ts +++ b/apps/backend/src/routes/bot-commands.http.ts @@ -18,7 +18,7 @@ import { UpdateBotSettingsResponse, } from "@hazel/domain/http" import { Redis } from "@hazel/effect-bun" -import { Context, Duration, Effect, Option, Schedule, Stream } from "effect" +import { ServiceMap, Duration, Effect, Option, Schedule, Stream } from "effect" import { HazelApi } from "../api.ts" import { BotGatewayService } from "../services/bot-gateway-service.ts" import { IntegrationTokenService } from "../services/integration-token-service.ts" @@ -107,7 +107,7 @@ interface CommandSseStreamOptions { readonly botId: string readonly botName: string readonly channel: string - readonly redis: Pick, "subscribe"> + readonly redis: Pick, "subscribe"> readonly heartbeatInterval?: Duration.DurationInput } diff --git a/apps/backend/src/routes/presence.http.ts b/apps/backend/src/routes/presence.http.ts index 7966ed38b..da249ae79 100644 --- a/apps/backend/src/routes/presence.http.ts +++ b/apps/backend/src/routes/presence.http.ts @@ -8,6 +8,7 @@ import { HazelApi } from "../api" export const HttpPresencePublicLive = HttpApiBuilder.group(HazelApi, "presencePublic", (handlers) => Effect.gen(function* () { const db = yield* Database.Database + const userPresenceStatusRepo = yield* UserPresenceStatusRepo return handlers.handle( "markOffline", @@ -16,7 +17,7 @@ export const HttpPresencePublicLive = HttpApiBuilder.group(HazelApi, "presencePu yield* db .transaction( Effect.asVoid( - UserPresenceStatusRepo.updateStatus({ + userPresenceStatusRepo.updateStatus({ userId: payload.userId, status: "offline", customMessage: null, diff --git a/apps/backend/src/routes/uploads.http.ts b/apps/backend/src/routes/uploads.http.ts index 6a02f9939..240ff009a 100644 --- a/apps/backend/src/routes/uploads.http.ts +++ b/apps/backend/src/routes/uploads.http.ts @@ -28,6 +28,9 @@ export const HttpUploadsLive = HttpApiBuilder.group(HazelApi, "uploads", (handle Effect.gen(function* () { const db = yield* Database.Database const s3 = yield* S3 + const attachmentPolicy = yield* AttachmentPolicy + const organizationPolicy = yield* OrganizationPolicy + const attachmentRepo = yield* AttachmentRepo return handlers.handle( "presign", @@ -148,7 +151,7 @@ export const HttpUploadsLive = HttpApiBuilder.group(HazelApi, "uploads", (handle } // Check if user is an admin or owner of the organization - yield* OrganizationPolicy.canUpdate(req.organizationId) + yield* organizationPolicy.canUpdate(req.organizationId) // Check rate limit (5 per hour) yield* checkAvatarRateLimit(user.id) @@ -189,7 +192,7 @@ export const HttpUploadsLive = HttpApiBuilder.group(HazelApi, "uploads", (handle Match.when({ type: "custom-emoji" }, (req) => Effect.gen(function* () { // Check if user is admin/owner of the org - yield* OrganizationPolicy.canUpdate(req.organizationId) + yield* organizationPolicy.canUpdate(req.organizationId) // Check rate limit (reuse avatar rate limit) yield* checkAvatarRateLimit(user.id) @@ -237,11 +240,11 @@ export const HttpUploadsLive = HttpApiBuilder.group(HazelApi, "uploads", (handle // Create attachment record with "uploading" status // Validates user has permission to upload to the specified channel/org - yield* AttachmentPolicy.canCreate() + yield* attachmentPolicy.canCreate() yield* db .transaction( Effect.gen(function* () { - yield* AttachmentRepo.insert({ + yield* attachmentRepo.insert({ id: attachmentId, uploadedBy: user.id, organizationId: req.organizationId, diff --git a/apps/backend/src/rpc/handlers/attachments.ts b/apps/backend/src/rpc/handlers/attachments.ts index 2f52e7266..2f583675d 100644 --- a/apps/backend/src/rpc/handlers/attachments.ts +++ b/apps/backend/src/rpc/handlers/attachments.ts @@ -9,14 +9,16 @@ import { AttachmentPolicy } from "../../policies/attachment-policy" export const AttachmentRpcLive = AttachmentRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database + const attachmentPolicy = yield* AttachmentPolicy + const attachmentRepo = yield* AttachmentRepo return { "attachment.delete": ({ id }) => db .transaction( Effect.gen(function* () { - yield* AttachmentPolicy.canDelete(id) - yield* AttachmentRepo.deleteById(id) + yield* attachmentPolicy.canDelete(id) + yield* attachmentRepo.deleteById(id) const txid = yield* generateTransactionId() @@ -29,8 +31,8 @@ export const AttachmentRpcLive = AttachmentRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* AttachmentPolicy.canUpdate(id) - const attachment = yield* AttachmentRepo.update({ id, status: "complete" }) + yield* attachmentPolicy.canUpdate(id) + const attachment = yield* attachmentRepo.update({ id, status: "complete" }) return attachment }), @@ -45,8 +47,8 @@ export const AttachmentRpcLive = AttachmentRpcs.toLayer( `Marking attachment ${id} as failed${reason ? `: ${reason}` : ""}`, ) - yield* AttachmentPolicy.canUpdate(id) - yield* AttachmentRepo.update({ id, status: "failed" }) + yield* attachmentPolicy.canUpdate(id) + yield* attachmentRepo.update({ id, status: "failed" }) }), ) .pipe(withRemapDbErrors("Attachment", "update")), diff --git a/apps/backend/src/rpc/handlers/bots.ts b/apps/backend/src/rpc/handlers/bots.ts index bf8220dd7..5713716e2 100644 --- a/apps/backend/src/rpc/handlers/bots.ts +++ b/apps/backend/src/rpc/handlers/bots.ts @@ -35,6 +35,8 @@ const generateBotToken = async (): Promise<{ token: string; tokenHash: string }> export const BotRpcLive = BotRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database + const botPolicy = yield* BotPolicy + const channelAccessSync = yield* ChannelAccessSyncService return { "bot.create": (payload) => @@ -53,7 +55,7 @@ export const BotRpcLive = BotRpcs.toLayer( return yield* db .transaction( Effect.gen(function* () { - yield* BotPolicy.canCreate(organizationId) + yield* botPolicy.canCreate(organizationId) const botRepo = yield* BotRepo const installationRepo = yield* BotInstallationRepo @@ -126,7 +128,7 @@ export const BotRpcLive = BotRpcs.toLayer( }), ) - yield* ChannelAccessSyncService.syncUserInOrganization( + yield* channelAccessSync.syncUserInOrganization( botUserId, organizationId, ) @@ -156,7 +158,7 @@ export const BotRpcLive = BotRpcs.toLayer( "bot.get": ({ id }) => Effect.gen(function* () { - yield* BotPolicy.canRead(id) + yield* botPolicy.canRead(id) const botRepo = yield* BotRepo const botOption = yield* botRepo.findById(id) @@ -182,7 +184,7 @@ export const BotRpcLive = BotRpcs.toLayer( return yield* db .transaction( Effect.gen(function* () { - yield* BotPolicy.canUpdate(id) + yield* botPolicy.canUpdate(id) const botRepo = yield* BotRepo // Check bot exists @@ -227,7 +229,7 @@ export const BotRpcLive = BotRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* BotPolicy.canDelete(id) + yield* botPolicy.canDelete(id) const botRepo = yield* BotRepo // Check bot exists @@ -256,7 +258,7 @@ export const BotRpcLive = BotRpcs.toLayer( return yield* db .transaction( Effect.gen(function* () { - yield* BotPolicy.canUpdate(id) + yield* botPolicy.canUpdate(id) const botRepo = yield* BotRepo // Check bot exists @@ -285,7 +287,7 @@ export const BotRpcLive = BotRpcs.toLayer( "bot.getCommands": ({ botId }) => Effect.gen(function* () { - yield* BotPolicy.canRead(botId) + yield* botPolicy.canRead(botId) const botRepo = yield* BotRepo const commandRepo = yield* BotCommandRepo @@ -393,7 +395,7 @@ export const BotRpcLive = BotRpcs.toLayer( return yield* db .transaction( Effect.gen(function* () { - yield* BotPolicy.canInstall(organizationId) + yield* botPolicy.canInstall(organizationId) const botRepo = yield* BotRepo const installationRepo = yield* BotInstallationRepo @@ -446,7 +448,7 @@ export const BotRpcLive = BotRpcs.toLayer( }), ) - yield* ChannelAccessSyncService.syncUserInOrganization( + yield* channelAccessSync.syncUserInOrganization( bot.userId, organizationId, ) @@ -478,7 +480,7 @@ export const BotRpcLive = BotRpcs.toLayer( return yield* db .transaction( Effect.gen(function* () { - yield* BotPolicy.canUninstall(organizationId) + yield* botPolicy.canUninstall(organizationId) const botRepo = yield* BotRepo const installationRepo = yield* BotInstallationRepo @@ -523,7 +525,7 @@ export const BotRpcLive = BotRpcs.toLayer( ), ) - yield* ChannelAccessSyncService.syncUserInOrganization( + yield* channelAccessSync.syncUserInOrganization( botOption.value.userId, organizationId, ) @@ -556,7 +558,7 @@ export const BotRpcLive = BotRpcs.toLayer( return yield* db .transaction( Effect.gen(function* () { - yield* BotPolicy.canInstall(organizationId) + yield* botPolicy.canInstall(organizationId) const botRepo = yield* BotRepo const installationRepo = yield* BotInstallationRepo @@ -606,7 +608,7 @@ export const BotRpcLive = BotRpcs.toLayer( }), ) - yield* ChannelAccessSyncService.syncUserInOrganization( + yield* channelAccessSync.syncUserInOrganization( bot.userId, organizationId, ) @@ -626,7 +628,7 @@ export const BotRpcLive = BotRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* BotPolicy.canUpdate(id) + yield* botPolicy.canUpdate(id) const botRepo = yield* BotRepo // Check bot exists diff --git a/apps/backend/src/rpc/handlers/channel-members.ts b/apps/backend/src/rpc/handlers/channel-members.ts index a0a0dbae1..8d90d7768 100644 --- a/apps/backend/src/rpc/handlers/channel-members.ts +++ b/apps/backend/src/rpc/handlers/channel-members.ts @@ -12,6 +12,12 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database const botGateway = yield* BotGatewayService + const channelMemberPolicy = yield* ChannelMemberPolicy + const channelMemberRepo = yield* ChannelMemberRepo + const channelRepo = yield* ChannelRepo + const channelAccessSync = yield* ChannelAccessSyncService + const organizationMemberRepo = yield* OrganizationMemberRepo + const notificationRepo = yield* NotificationRepo return { "channelMember.create": (payload) => @@ -20,8 +26,8 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( Effect.gen(function* () { const user = yield* CurrentUser.Context - yield* ChannelMemberPolicy.canCreate(payload.channelId) - const createdChannelMember = yield* ChannelMemberRepo.insert({ + yield* channelMemberPolicy.canCreate(payload.channelId) + const createdChannelMember = yield* channelMemberRepo.insert({ channelId: payload.channelId, userId: user.id, isHidden: false, @@ -33,9 +39,9 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( deletedAt: null, }).pipe(Effect.map((res) => res[0]!)) - const channelOption = yield* ChannelRepo.findById(payload.channelId) + const channelOption = yield* channelRepo.findById(payload.channelId) if (Option.isSome(channelOption)) { - yield* ChannelAccessSyncService.syncUserInOrganization( + yield* channelAccessSync.syncUserInOrganization( user.id, channelOption.value.organizationId, ) @@ -67,8 +73,8 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* ChannelMemberPolicy.canUpdate(id) - const updatedChannelMember = yield* ChannelMemberRepo.update({ + yield* channelMemberPolicy.canUpdate(id) + const updatedChannelMember = yield* channelMemberRepo.update({ id, ...payload, }) @@ -85,21 +91,21 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( "channelMember.delete": ({ id }) => Effect.gen(function* () { - const deletedMemberOption = yield* ChannelMemberRepo.findById(id).pipe( + const deletedMemberOption = yield* channelMemberRepo.findById(id).pipe( withRemapDbErrors("ChannelMember", "select"), ) const response = yield* db .transaction( Effect.gen(function* () { - yield* ChannelMemberPolicy.canDelete(id) - yield* ChannelMemberRepo.deleteById(id) + yield* channelMemberPolicy.canDelete(id) + yield* channelMemberRepo.deleteById(id) if (Option.isSome(deletedMemberOption)) { - const channelOption = yield* ChannelRepo.findById( + const channelOption = yield* channelRepo.findById( deletedMemberOption.value.channelId, ).pipe(withRemapDbErrors("Channel", "select")) if (Option.isSome(channelOption)) { - yield* ChannelAccessSyncService.syncUserInOrganization( + yield* channelAccessSync.syncUserInOrganization( deletedMemberOption.value.userId, channelOption.value.organizationId, ) @@ -137,20 +143,20 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( const user = yield* CurrentUser.Context // Find the channel member record for this user and channel - yield* ChannelMemberPolicy.canRead(channelId) - const memberOption = yield* ChannelMemberRepo.findByChannelAndUser( + yield* channelMemberPolicy.canRead(channelId) + const memberOption = yield* channelMemberRepo.findByChannelAndUser( channelId, user.id, ).pipe(withRemapDbErrors("ChannelMember", "select")) // Get channel to find organizationId - const channelOption = yield* ChannelRepo.findById(channelId).pipe( + const channelOption = yield* channelRepo.findById(channelId).pipe( withRemapDbErrors("Channel", "select"), ) // Get organization member for notification deletion const orgMemberOption = Option.isSome(channelOption) - ? yield* OrganizationMemberRepo.findByOrgAndUser( + ? yield* organizationMemberRepo.findByOrgAndUser( channelOption.value.organizationId, user.id, ).pipe(withRemapDbErrors("OrganizationMember", "select")) @@ -162,8 +168,8 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( Effect.gen(function* () { // If member exists, clear the notification count if (Option.isSome(memberOption)) { - yield* ChannelMemberPolicy.canUpdate(memberOption.value.id) - yield* ChannelMemberRepo.update({ + yield* channelMemberPolicy.canUpdate(memberOption.value.id) + yield* channelMemberRepo.update({ id: memberOption.value.id, notificationCount: 0, }) @@ -171,7 +177,7 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( // Delete all notifications for this channel if (Option.isSome(orgMemberOption)) { - yield* NotificationRepo.deleteByChannelId( + yield* notificationRepo.deleteByChannelId( channelId, orgMemberOption.value.id, ) diff --git a/apps/backend/src/rpc/handlers/channel-sections.ts b/apps/backend/src/rpc/handlers/channel-sections.ts index f3be3012a..344b5f5e8 100644 --- a/apps/backend/src/rpc/handlers/channel-sections.ts +++ b/apps/backend/src/rpc/handlers/channel-sections.ts @@ -13,6 +13,9 @@ import { OrgResolver } from "../../services/org-resolver" export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database + const channelSectionPolicy = yield* ChannelSectionPolicy + const channelRepo = yield* ChannelRepo + const channelSectionRepo = yield* ChannelSectionRepo return { "channelSection.create": ({ id, ...payload }) => @@ -44,8 +47,8 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( ? { id, ...payload, order, deletedAt: null } : { ...payload, order, deletedAt: null } - yield* ChannelSectionPolicy.canCreate(payload.organizationId) - const createdSection = yield* ChannelSectionRepo.insert( + yield* channelSectionPolicy.canCreate(payload.organizationId) + const createdSection = yield* channelSectionRepo.insert( insertData as typeof payload & { order: number; deletedAt: null }, ).pipe(Effect.map((res) => res[0]!)) @@ -63,8 +66,8 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* ChannelSectionPolicy.canUpdate(id) - const updatedSection = yield* ChannelSectionRepo.update({ + yield* channelSectionPolicy.canUpdate(id) + const updatedSection = yield* channelSectionRepo.update({ id, ...payload, }) @@ -83,9 +86,9 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* ChannelSectionPolicy.canDelete(id) + yield* channelSectionPolicy.canDelete(id) // First, move all channels in this section back to default (sectionId = null) - const section = yield* ChannelSectionRepo.findById(id) + const section = yield* channelSectionRepo.findById(id) if (Option.isNone(section)) { return yield* Effect.fail(new ChannelSectionNotFoundError({ sectionId: id })) @@ -100,7 +103,7 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( ) // Delete the section - yield* ChannelSectionRepo.deleteById(id) + yield* channelSectionRepo.deleteById(id) const txid = yield* generateTransactionId() @@ -113,7 +116,7 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* ChannelSectionPolicy.canReorder(organizationId) + yield* channelSectionPolicy.canReorder(organizationId) yield* transactionAwareExecute((client) => client .update(schema.channelSectionsTable) @@ -143,14 +146,14 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( .transaction( Effect.gen(function* () { // Get channel first to know its organization - const channel = yield* ChannelRepo.findById(channelId) + const channel = yield* channelRepo.findById(channelId) if (Option.isNone(channel)) { return yield* Effect.fail(new ChannelNotFoundError({ channelId })) } // Validate target section exists and belongs to same org if (sectionId !== null) { - const section = yield* ChannelSectionRepo.findById(sectionId) + const section = yield* channelSectionRepo.findById(sectionId) if (Option.isNone(section)) { return yield* Effect.fail(new ChannelSectionNotFoundError({ sectionId })) } @@ -160,7 +163,7 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( } if (sectionId !== null) { - yield* ChannelSectionPolicy.canUpdate(sectionId) + yield* channelSectionPolicy.canUpdate(sectionId) } else { yield* ErrorUtils.refailUnauthorized( "ChannelSection", @@ -177,7 +180,7 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( ) } // Update the channel's sectionId - yield* ChannelRepo.update({ + yield* channelRepo.update({ id: channelId, sectionId, }) diff --git a/apps/backend/src/rpc/handlers/channel-webhooks.ts b/apps/backend/src/rpc/handlers/channel-webhooks.ts index 2d2db37fb..77a5d0253 100644 --- a/apps/backend/src/rpc/handlers/channel-webhooks.ts +++ b/apps/backend/src/rpc/handlers/channel-webhooks.ts @@ -42,6 +42,7 @@ const buildWebhookUrl = (webhookId: string, token: string) => { export const ChannelWebhookRpcLive = ChannelWebhookRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database + const channelWebhookPolicy = yield* ChannelWebhookPolicy return { "channelWebhook.create": (payload) => @@ -86,7 +87,7 @@ export const ChannelWebhookRpcLive = ChannelWebhookRpcs.toLayer( }), ) - yield* ChannelWebhookPolicy.canCreate(payload.channelId) + yield* channelWebhookPolicy.canCreate(payload.channelId) // Create webhook const [webhook] = yield* webhookRepo.insert({ @@ -120,7 +121,7 @@ export const ChannelWebhookRpcLive = ChannelWebhookRpcs.toLayer( Effect.gen(function* () { const webhookRepo = yield* ChannelWebhookRepo - yield* ChannelWebhookPolicy.canRead(channelId) + yield* channelWebhookPolicy.canRead(channelId) const webhooks = yield* webhookRepo.findByChannel(channelId) return new ChannelWebhookListResponse({ data: webhooks }) @@ -133,7 +134,7 @@ export const ChannelWebhookRpcLive = ChannelWebhookRpcs.toLayer( const webhookRepo = yield* ChannelWebhookRepo const botService = yield* WebhookBotService - yield* ChannelWebhookPolicy.canUpdate(id) + yield* channelWebhookPolicy.canUpdate(id) // Get current webhook const webhookOption = yield* webhookRepo.findById(id) @@ -178,7 +179,7 @@ export const ChannelWebhookRpcLive = ChannelWebhookRpcs.toLayer( Effect.gen(function* () { const webhookRepo = yield* ChannelWebhookRepo - yield* ChannelWebhookPolicy.canUpdate(id) + yield* channelWebhookPolicy.canUpdate(id) // Get current webhook const webhookOption = yield* webhookRepo.findById(id) @@ -214,7 +215,7 @@ export const ChannelWebhookRpcLive = ChannelWebhookRpcs.toLayer( Effect.gen(function* () { const webhookRepo = yield* ChannelWebhookRepo - yield* ChannelWebhookPolicy.canDelete(id) + yield* channelWebhookPolicy.canDelete(id) // Soft delete webhook only - bot user cleanup can be handled separately // (e.g., by admin or background job) since integrations may create webhooks diff --git a/apps/backend/src/rpc/handlers/channels.ts b/apps/backend/src/rpc/handlers/channels.ts index c76da4dac..00f7fe02f 100644 --- a/apps/backend/src/rpc/handlers/channels.ts +++ b/apps/backend/src/rpc/handlers/channels.ts @@ -30,6 +30,14 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database const botGateway = yield* BotGatewayService + const channelMemberRepo = yield* ChannelMemberRepo + const channelPolicy = yield* ChannelPolicy + const userPolicy = yield* UserPolicy + const messageRepo = yield* MessageRepo + const channelRepo = yield* ChannelRepo + const organizationMemberRepo = yield* OrganizationMemberRepo + const userRepo = yield* UserRepo + const channelAccessSync = yield* ChannelAccessSyncService return { "channel.create": ({ id, addAllMembers, ...payload }) => @@ -42,12 +50,12 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( ? { id, ...payload, deletedAt: null } : { ...payload, deletedAt: null } - yield* ChannelPolicy.canCreate(payload.organizationId) - const createdChannel = yield* ChannelRepo.insert( + yield* channelPolicy.canCreate(payload.organizationId) + const createdChannel = yield* channelRepo.insert( insertData as typeof payload & { deletedAt: null }, ).pipe(Effect.map((res) => res[0]!)) - yield* ChannelMemberRepo.insert({ + yield* channelMemberRepo.insert({ channelId: createdChannel.id, userId: user.id, isHidden: false, @@ -60,14 +68,14 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( }) if (addAllMembers) { - const orgMembers = yield* OrganizationMemberRepo.findAllByOrganization( + const orgMembers = yield* organizationMemberRepo.findAllByOrganization( payload.organizationId, ) yield* Effect.forEach( orgMembers.filter((m) => m.userId !== user.id), (member) => - ChannelMemberRepo.insert({ + channelMemberRepo.insert({ channelId: createdChannel.id, userId: member.userId, isHidden: false, @@ -82,7 +90,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( ) } - yield* ChannelAccessSyncService.syncChannel(createdChannel.id) + yield* channelAccessSync.syncChannel(createdChannel.id) const txid = yield* generateTransactionId() @@ -110,14 +118,14 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* ChannelPolicy.canUpdate(id) - const updatedChannel = yield* ChannelRepo.update({ + yield* channelPolicy.canUpdate(id) + const updatedChannel = yield* channelRepo.update({ id, ...payload, }) - yield* ChannelAccessSyncService.syncChannel(id) - yield* ChannelAccessSyncService.syncChildThreads(id) + yield* channelAccessSync.syncChannel(id) + yield* channelAccessSync.syncChildThreads(id) const txid = yield* generateTransactionId() @@ -143,16 +151,16 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( "channel.delete": ({ id }) => Effect.gen(function* () { - const existingChannel = yield* ChannelRepo.findById(id).pipe( + const existingChannel = yield* channelRepo.findById(id).pipe( withRemapDbErrors("Channel", "select"), ) const response = yield* db .transaction( Effect.gen(function* () { - yield* ChannelPolicy.canDelete(id) - yield* ChannelRepo.deleteById(id) - yield* ChannelAccessSyncService.removeChannel(id) - yield* ChannelAccessSyncService.syncChildThreads(id) + yield* channelPolicy.canDelete(id) + yield* channelRepo.deleteById(id) + yield* channelAccessSync.removeChannel(id) + yield* channelAccessSync.syncChildThreads(id) const txid = yield* generateTransactionId() @@ -193,7 +201,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( // Check for existing DM channel if (payload.type === "single") { - const existingChannel = yield* ChannelMemberRepo.findExistingSingleDmChannel( + const existingChannel = yield* channelMemberRepo.findExistingSingleDmChannel( user.id, payload.participantIds[0], OrganizationId.make(payload.organizationId), @@ -212,9 +220,9 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( // Generate channel name for DMs let channelName = payload.name if (payload.type === "single") { - yield* UserPolicy.canRead(payload.participantIds[0]!) - const otherUser = yield* UserRepo.findById(payload.participantIds[0]) - const currentUser = yield* UserRepo.findById(user.id) + yield* userPolicy.canRead(payload.participantIds[0]!) + const otherUser = yield* userRepo.findById(payload.participantIds[0]) + const currentUser = yield* userRepo.findById(user.id) if (Option.isSome(otherUser) && Option.isSome(currentUser)) { // Create a consistent name for DMs using first and last name @@ -228,8 +236,8 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( } // Create channel - yield* ChannelPolicy.canCreate(OrganizationId.make(payload.organizationId)) - const createdChannel = yield* ChannelRepo.insert({ + yield* channelPolicy.canCreate(OrganizationId.make(payload.organizationId)) + const createdChannel = yield* channelRepo.insert({ name: channelName || "Group Channel", icon: null, type: payload.type, @@ -240,7 +248,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( }).pipe(Effect.map((res) => res[0]!)) // Add creator as member - yield* ChannelMemberRepo.insert({ + yield* channelMemberRepo.insert({ channelId: createdChannel.id, userId: user.id, isHidden: false, @@ -254,7 +262,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( // Add all participants as members for (const participantId of payload.participantIds) { - yield* ChannelMemberRepo.insert({ + yield* channelMemberRepo.insert({ channelId: createdChannel.id, userId: participantId, isHidden: false, @@ -267,7 +275,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( }) } - yield* ChannelAccessSyncService.syncChannel(createdChannel.id) + yield* channelAccessSync.syncChannel(createdChannel.id) const txid = yield* generateTransactionId() @@ -298,7 +306,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( const user = yield* CurrentUser.Context // 1. Find the message and resolve thread context from authoritative DB data - const message = yield* MessageRepo.findById(messageId) + const message = yield* messageRepo.findById(messageId) if (Option.isNone(message)) { return yield* Effect.fail(new MessageNotFoundError({ messageId })) @@ -306,7 +314,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( // If the message already points to a thread, return that thread. if (message.value.threadChannelId) { - const existingThread = yield* ChannelRepo.findById( + const existingThread = yield* channelRepo.findById( message.value.threadChannelId, ) if (Option.isNone(existingThread)) { @@ -318,7 +326,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( ) } - yield* ChannelAccessSyncService.syncChannel(existingThread.value.id) + yield* channelAccessSync.syncChannel(existingThread.value.id) const txid = yield* generateTransactionId() return { @@ -327,7 +335,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( } } - const parentChannel = yield* ChannelRepo.findById(message.value.channelId) + const parentChannel = yield* channelRepo.findById(message.value.channelId) if (Option.isNone(parentChannel)) { return yield* Effect.fail( new InternalServerError({ @@ -339,7 +347,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( // If the message is already in a thread channel, return that thread. if (parentChannel.value.type === "thread") { - yield* ChannelAccessSyncService.syncChannel(parentChannel.value.id) + yield* channelAccessSync.syncChannel(parentChannel.value.id) const txid = yield* generateTransactionId() return { @@ -383,13 +391,13 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( deletedAt: null, } - yield* ChannelPolicy.canCreate(organizationId) - const createdChannel = yield* ChannelRepo.insert(insertData).pipe( + yield* channelPolicy.canCreate(organizationId) + const createdChannel = yield* channelRepo.insert(insertData).pipe( Effect.map((res) => res[0]!), ) // 3. Add creator as member - yield* ChannelMemberRepo.insert({ + yield* channelMemberRepo.insert({ channelId: createdChannel.id, userId: user.id, isHidden: false, @@ -409,7 +417,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( .where(eq(schema.messagesTable.id, messageId)), ) - yield* ChannelAccessSyncService.syncChannel(createdChannel.id) + yield* channelAccessSync.syncChannel(createdChannel.id) const txid = yield* generateTransactionId() @@ -423,9 +431,9 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( "channel.generateName": ({ channelId }) => Effect.gen(function* () { - yield* ChannelPolicy.canUpdate(channelId) + yield* channelPolicy.canUpdate(channelId) - const channel = yield* ChannelRepo.findById(channelId).pipe( + const channel = yield* channelRepo.findById(channelId).pipe( Effect.catchTag("DatabaseError", (err) => Effect.fail( new InternalServerError({ diff --git a/apps/backend/src/rpc/handlers/chat-sync.ts b/apps/backend/src/rpc/handlers/chat-sync.ts index 22523b939..404e89962 100644 --- a/apps/backend/src/rpc/handlers/chat-sync.ts +++ b/apps/backend/src/rpc/handlers/chat-sync.ts @@ -39,13 +39,14 @@ export const ChatSyncRpcLive = ChatSyncRpcs.toLayer( const connectionRepo = yield* ChatSyncConnectionRepo const channelLinkRepo = yield* ChatSyncChannelLinkRepo const integrationConnectionRepo = yield* IntegrationConnectionRepo + const integrationConnectionPolicy = yield* IntegrationConnectionPolicy return { "chatSync.connection.create": (payload) => db .transaction( Effect.gen(function* () { - yield* IntegrationConnectionPolicy.canInsert(payload.organizationId) + yield* integrationConnectionPolicy.canInsert(payload.organizationId) const currentUser = yield* CurrentUser.Context const integrationConnectionId = yield* Effect.gen(function* () { @@ -134,7 +135,7 @@ export const ChatSyncRpcLive = ChatSyncRpcs.toLayer( "chatSync.connection.list": ({ organizationId }) => Effect.gen(function* () { - yield* IntegrationConnectionPolicy.canSelect(organizationId) + yield* integrationConnectionPolicy.canSelect(organizationId) const data = yield* connectionRepo.findByOrganization(organizationId) return new ChatSyncConnectionListResponse({ data }) }).pipe( @@ -161,7 +162,7 @@ export const ChatSyncRpcLive = ChatSyncRpcs.toLayer( ) } const connection = connectionOption.value - yield* IntegrationConnectionPolicy.canDelete(connection.organizationId) + yield* integrationConnectionPolicy.canDelete(connection.organizationId) yield* connectionRepo.softDelete(syncConnectionId) const links = yield* channelLinkRepo.findBySyncConnection(syncConnectionId) @@ -197,7 +198,7 @@ export const ChatSyncRpcLive = ChatSyncRpcs.toLayer( ) } const connection = connectionOption.value - yield* IntegrationConnectionPolicy.canInsert(connection.organizationId) + yield* integrationConnectionPolicy.canInsert(connection.organizationId) const existingHazel = yield* channelLinkRepo.findByHazelChannel( payload.syncConnectionId, @@ -280,7 +281,7 @@ export const ChatSyncRpcLive = ChatSyncRpcs.toLayer( ) } const connection = connectionOption.value - yield* IntegrationConnectionPolicy.canSelect(connection.organizationId) + yield* integrationConnectionPolicy.canSelect(connection.organizationId) const data = yield* channelLinkRepo.findBySyncConnection(syncConnectionId) return new ChatSyncChannelLinkListResponse({ data }) @@ -318,7 +319,7 @@ export const ChatSyncRpcLive = ChatSyncRpcs.toLayer( }), ) } - yield* IntegrationConnectionPolicy.canDelete( + yield* integrationConnectionPolicy.canDelete( connectionOption.value.organizationId, ) @@ -360,7 +361,7 @@ export const ChatSyncRpcLive = ChatSyncRpcs.toLayer( }), ) } - yield* IntegrationConnectionPolicy.canUpdate( + yield* integrationConnectionPolicy.canUpdate( connectionOption.value.organizationId, ) diff --git a/apps/backend/src/rpc/handlers/custom-emojis.ts b/apps/backend/src/rpc/handlers/custom-emojis.ts index 6e2f3db16..1abf6e641 100644 --- a/apps/backend/src/rpc/handlers/custom-emojis.ts +++ b/apps/backend/src/rpc/handlers/custom-emojis.ts @@ -14,6 +14,8 @@ import { CustomEmojiPolicy } from "../../policies/custom-emoji-policy" export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database + const customEmojiPolicy = yield* CustomEmojiPolicy + const customEmojiRepo = yield* CustomEmojiRepo return { "customEmoji.create": (payload) => @@ -23,7 +25,7 @@ export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( const user = yield* CurrentUser.Context // Check name uniqueness - const existing = yield* CustomEmojiRepo.findByOrgAndName( + const existing = yield* customEmojiRepo.findByOrgAndName( payload.organizationId, payload.name, ) @@ -37,7 +39,7 @@ export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( } // Check if a soft-deleted emoji with same name exists - const deleted = yield* CustomEmojiRepo.findDeletedByOrgAndName( + const deleted = yield* customEmojiRepo.findDeletedByOrgAndName( payload.organizationId, payload.name, ) @@ -52,8 +54,8 @@ export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( ) } - yield* CustomEmojiPolicy.canCreate(payload.organizationId) - const created = yield* CustomEmojiRepo.insert({ + yield* customEmojiPolicy.canCreate(payload.organizationId) + const created = yield* customEmojiRepo.insert({ organizationId: payload.organizationId, name: payload.name, imageUrl: payload.imageUrl, @@ -75,14 +77,14 @@ export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( .transaction( Effect.gen(function* () { // Check if emoji exists - const existing = yield* CustomEmojiRepo.findById(id) + const existing = yield* customEmojiRepo.findById(id) if (Option.isNone(existing)) { return yield* Effect.fail(new CustomEmojiNotFoundError({ customEmojiId: id })) } // Check name uniqueness if renaming if (payload.name !== undefined) { - const nameConflict = yield* CustomEmojiRepo.findByOrgAndName( + const nameConflict = yield* customEmojiRepo.findByOrgAndName( existing.value.organizationId, payload.name, ) @@ -96,8 +98,8 @@ export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( } } - yield* CustomEmojiPolicy.canUpdate(id) - const updated = yield* CustomEmojiRepo.update({ + yield* customEmojiPolicy.canUpdate(id) + const updated = yield* customEmojiRepo.update({ id, ...payload, }) @@ -117,13 +119,13 @@ export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( .transaction( Effect.gen(function* () { // Check existence first so missing IDs map to NotFound (not Unauthorized). - const existing = yield* CustomEmojiRepo.findById(id) + const existing = yield* customEmojiRepo.findById(id) if (Option.isNone(existing) || existing.value.deletedAt !== null) { return yield* Effect.fail(new CustomEmojiNotFoundError({ customEmojiId: id })) } - yield* CustomEmojiPolicy.canDelete(id) - const deleted = yield* CustomEmojiRepo.softDelete(id) + yield* customEmojiPolicy.canDelete(id) + const deleted = yield* customEmojiRepo.softDelete(id) if (Option.isNone(deleted)) { return yield* Effect.fail(new CustomEmojiNotFoundError({ customEmojiId: id })) @@ -141,13 +143,13 @@ export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( .transaction( Effect.gen(function* () { // Look up the deleted emoji first - const existing = yield* CustomEmojiRepo.findById(id) + const existing = yield* customEmojiRepo.findById(id) if (Option.isNone(existing) || existing.value.deletedAt === null) { return yield* Effect.fail(new CustomEmojiNotFoundError({ customEmojiId: id })) } // Check that no active emoji with the same name exists - const nameConflict = yield* CustomEmojiRepo.findByOrgAndName( + const nameConflict = yield* customEmojiRepo.findByOrgAndName( existing.value.organizationId, existing.value.name, ) @@ -160,8 +162,8 @@ export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( ) } - yield* CustomEmojiPolicy.canCreate(existing.value.organizationId) - const restored = yield* CustomEmojiRepo.restore(id, imageUrl) + yield* customEmojiPolicy.canCreate(existing.value.organizationId) + const restored = yield* customEmojiRepo.restore(id, imageUrl) if (Option.isNone(restored)) { return yield* Effect.fail(new CustomEmojiNotFoundError({ customEmojiId: id })) diff --git a/apps/backend/src/rpc/handlers/github-subscriptions.ts b/apps/backend/src/rpc/handlers/github-subscriptions.ts index f1a863790..f70d4d166 100644 --- a/apps/backend/src/rpc/handlers/github-subscriptions.ts +++ b/apps/backend/src/rpc/handlers/github-subscriptions.ts @@ -27,6 +27,7 @@ export const GitHubSubscriptionRpcLive = GitHubSubscriptionRpcs.toLayer( const channelRepo = yield* ChannelRepo const subscriptionRepo = yield* GitHubSubscriptionRepo const integrationRepo = yield* IntegrationConnectionRepo + const gitHubSubscriptionPolicy = yield* GitHubSubscriptionPolicy return { "githubSubscription.create": (payload) => @@ -67,7 +68,7 @@ export const GitHubSubscriptionRpcLive = GitHubSubscriptionRpcs.toLayer( ) } - yield* GitHubSubscriptionPolicy.canCreate(payload.channelId) + yield* gitHubSubscriptionPolicy.canCreate(payload.channelId) // Create subscription const [subscription] = yield* subscriptionRepo.insert({ @@ -96,7 +97,7 @@ export const GitHubSubscriptionRpcLive = GitHubSubscriptionRpcs.toLayer( "githubSubscription.list": ({ channelId }) => Effect.gen(function* () { - yield* GitHubSubscriptionPolicy.canRead(channelId) + yield* gitHubSubscriptionPolicy.canRead(channelId) const subscriptions = yield* subscriptionRepo.findByChannel(channelId) return new GitHubSubscriptionListResponse({ data: subscriptions }) @@ -113,7 +114,7 @@ export const GitHubSubscriptionRpcLive = GitHubSubscriptionRpcs.toLayer( const organizationId = user.organizationId - yield* GitHubSubscriptionPolicy.canReadByOrganization(organizationId) + yield* gitHubSubscriptionPolicy.canReadByOrganization(organizationId) const subscriptions = yield* subscriptionRepo.findByOrganization(organizationId) return new GitHubSubscriptionListResponse({ data: subscriptions }) @@ -131,7 +132,7 @@ export const GitHubSubscriptionRpcLive = GitHubSubscriptionRpcs.toLayer( ) } - yield* GitHubSubscriptionPolicy.canUpdate(id) + yield* gitHubSubscriptionPolicy.canUpdate(id) // Update subscription const [updatedSubscription] = yield* subscriptionRepo.updateSettings(id, { @@ -154,7 +155,7 @@ export const GitHubSubscriptionRpcLive = GitHubSubscriptionRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* GitHubSubscriptionPolicy.canDelete(id) + yield* gitHubSubscriptionPolicy.canDelete(id) // Check subscription exists const subscriptionOption = yield* subscriptionRepo.findById(id) diff --git a/apps/backend/src/rpc/handlers/invitations.ts b/apps/backend/src/rpc/handlers/invitations.ts index 99e17ce33..065528248 100644 --- a/apps/backend/src/rpc/handlers/invitations.ts +++ b/apps/backend/src/rpc/handlers/invitations.ts @@ -17,6 +17,8 @@ export const InvitationRpcLive = InvitationRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database const workos = yield* WorkOS + const invitationPolicy = yield* InvitationPolicy + const invitationRepo = yield* InvitationRepo return { "invitation.create": (payload) => @@ -60,8 +62,8 @@ export const InvitationRpcLive = InvitationRpcs.toLayer( expiresAt.setDate(expiresAt.getDate() + 7) // Store invitation in local database - yield* InvitationPolicy.canCreate(payload.organizationId) - const createdInvitation = yield* InvitationRepo.upsertByWorkosId({ + yield* invitationPolicy.canCreate(payload.organizationId) + const createdInvitation = yield* invitationRepo.upsertByWorkosId({ workosInvitationId: Schema.decodeUnknownSync(WorkOSInvitationId)( workosInvitation.id, ), @@ -117,8 +119,8 @@ export const InvitationRpcLive = InvitationRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* InvitationPolicy.canRead(invitationId) - const invitationOption = yield* InvitationRepo.findById(invitationId) + yield* invitationPolicy.canRead(invitationId) + const invitationOption = yield* invitationRepo.findById(invitationId) if (Option.isNone(invitationOption)) { return yield* Effect.fail(new InvitationNotFoundError({ invitationId })) } @@ -126,7 +128,7 @@ export const InvitationRpcLive = InvitationRpcs.toLayer( const invitation = invitationOption.value // Resend invitation via WorkOS (send new invitation to same email) - yield* InvitationPolicy.canUpdate(invitationId) + yield* invitationPolicy.canUpdate(invitationId) yield* workos .call((client) => client.userManagement.sendInvitation({ @@ -162,8 +164,8 @@ export const InvitationRpcLive = InvitationRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* InvitationPolicy.canRead(invitationId) - const invitationOption = yield* InvitationRepo.findById(invitationId) + yield* invitationPolicy.canRead(invitationId) + const invitationOption = yield* invitationRepo.findById(invitationId) if (Option.isNone(invitationOption)) { return yield* Effect.fail(new InvitationNotFoundError({ invitationId })) @@ -187,8 +189,8 @@ export const InvitationRpcLive = InvitationRpcs.toLayer( ), ) - yield* InvitationPolicy.canUpdate(invitationId) - yield* InvitationRepo.updateStatus(invitationId, "revoked") + yield* invitationPolicy.canUpdate(invitationId) + yield* invitationRepo.updateStatus(invitationId, "revoked") const txid = yield* generateTransactionId() @@ -204,8 +206,8 @@ export const InvitationRpcLive = InvitationRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* InvitationPolicy.canUpdate(id) - const updatedInvitation = yield* InvitationRepo.update({ + yield* invitationPolicy.canUpdate(id) + const updatedInvitation = yield* invitationRepo.update({ id, ...payload, }) @@ -227,8 +229,8 @@ export const InvitationRpcLive = InvitationRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* InvitationPolicy.canDelete(id) - yield* InvitationRepo.deleteById(id) + yield* invitationPolicy.canDelete(id) + yield* invitationRepo.deleteById(id) const txid = yield* generateTransactionId() diff --git a/apps/backend/src/rpc/handlers/message-reactions.ts b/apps/backend/src/rpc/handlers/message-reactions.ts index c77e5c3e3..7841f50d8 100644 --- a/apps/backend/src/rpc/handlers/message-reactions.ts +++ b/apps/backend/src/rpc/handlers/message-reactions.ts @@ -13,6 +13,8 @@ export const MessageReactionRpcLive = MessageReactionRpcs.toLayer( const db = yield* Database.Database const outboxRepo = yield* MessageOutboxRepo const connectConversationService = yield* ConnectConversationService + const messageReactionPolicy = yield* MessageReactionPolicy + const messageReactionRepo = yield* MessageReactionRepo return { "messageReaction.toggle": (payload) => @@ -23,8 +25,8 @@ export const MessageReactionRpcLive = MessageReactionRpcs.toLayer( const user = yield* CurrentUser.Context const { messageId, channelId, emoji } = payload - yield* MessageReactionPolicy.canList(messageId) - const existingReaction = yield* MessageReactionRepo.findByMessageUserEmoji( + yield* messageReactionPolicy.canList(messageId) + const existingReaction = yield* messageReactionRepo.findByMessageUserEmoji( messageId, user.id, emoji, @@ -41,8 +43,8 @@ export const MessageReactionRpcLive = MessageReactionRpcs.toLayer( emoji: existingReaction.value.emoji, userId: existingReaction.value.userId, } as const - yield* MessageReactionPolicy.canDelete(existingReaction.value.id) - yield* MessageReactionRepo.deleteById(existingReaction.value.id) + yield* messageReactionPolicy.canDelete(existingReaction.value.id) + yield* messageReactionRepo.deleteById(existingReaction.value.id) yield* outboxRepo.insert({ eventType: "reaction_deleted", aggregateId: existingReaction.value.id, @@ -64,10 +66,10 @@ export const MessageReactionRpcLive = MessageReactionRpcs.toLayer( } // Otherwise, create a new reaction - yield* MessageReactionPolicy.canCreate(messageId) + yield* messageReactionPolicy.canCreate(messageId) const conversationId = yield* connectConversationService.getConversationIdForChannel(channelId) - const createdMessageReaction = yield* MessageReactionRepo.insert({ + const createdMessageReaction = yield* messageReactionRepo.insert({ messageId, channelId, conversationId, @@ -108,12 +110,12 @@ export const MessageReactionRpcLive = MessageReactionRpcs.toLayer( Effect.gen(function* () { const user = yield* CurrentUser.Context - yield* MessageReactionPolicy.canCreate(payload.messageId) + yield* messageReactionPolicy.canCreate(payload.messageId) const conversationId = yield* connectConversationService.getConversationIdForChannel( payload.channelId, ) - const createdMessageReaction = yield* MessageReactionRepo.insert({ + const createdMessageReaction = yield* messageReactionRepo.insert({ ...payload, conversationId, userId: user.id, @@ -145,8 +147,8 @@ export const MessageReactionRpcLive = MessageReactionRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* MessageReactionPolicy.canUpdate(id) - const updatedMessageReaction = yield* MessageReactionRepo.update({ + yield* messageReactionPolicy.canUpdate(id) + const updatedMessageReaction = yield* messageReactionRepo.update({ id, ...payload, }) @@ -166,7 +168,7 @@ export const MessageReactionRpcLive = MessageReactionRpcs.toLayer( const txResult = yield* db .transaction( Effect.gen(function* () { - const existing = yield* MessageReactionRepo.findById(id) + const existing = yield* messageReactionRepo.findById(id) const deletedSyncPayload = Option.match(existing, { onNone: () => null as null, onSome: (value) => @@ -183,8 +185,8 @@ export const MessageReactionRpcLive = MessageReactionRpcs.toLayer( }, }) - yield* MessageReactionPolicy.canDelete(id) - yield* MessageReactionRepo.deleteById(id) + yield* messageReactionPolicy.canDelete(id) + yield* messageReactionRepo.deleteById(id) if (deletedSyncPayload !== null && Option.isSome(existing)) { yield* outboxRepo.insert({ diff --git a/apps/backend/src/rpc/handlers/messages.ts b/apps/backend/src/rpc/handlers/messages.ts index b13136639..0a18fa22c 100644 --- a/apps/backend/src/rpc/handlers/messages.ts +++ b/apps/backend/src/rpc/handlers/messages.ts @@ -30,6 +30,10 @@ export const MessageRpcLive = MessageRpcs.toLayer( const botGateway = yield* BotGatewayService const outboxRepo = yield* MessageOutboxRepo const connectConversationService = yield* ConnectConversationService + const messagePolicy = yield* MessagePolicy + const messageRepo = yield* MessageRepo + const attachmentPolicy = yield* AttachmentPolicy + const attachmentRepo = yield* AttachmentRepo return { "message.create": ({ attachmentIds, ...messageData }) => @@ -42,12 +46,12 @@ export const MessageRpcLive = MessageRpcs.toLayer( const response = yield* db .transaction( Effect.gen(function* () { - yield* MessagePolicy.canCreate(messageData.channelId) + yield* messagePolicy.canCreate(messageData.channelId) const conversationId = yield* connectConversationService.getConversationIdForChannel( messageData.channelId, ) - const createdMessage = yield* MessageRepo.insert({ + const createdMessage = yield* messageRepo.insert({ ...messageData, conversationId, authorId: user.id, @@ -58,8 +62,8 @@ export const MessageRpcLive = MessageRpcs.toLayer( if (attachmentIds && attachmentIds.length > 0) { yield* Effect.forEach(attachmentIds, (attachmentId) => Effect.gen(function* () { - yield* AttachmentPolicy.canUpdate(attachmentId) - yield* AttachmentRepo.update({ + yield* attachmentPolicy.canUpdate(attachmentId) + yield* attachmentRepo.update({ id: attachmentId, messageId: createdMessage.id, }) @@ -112,8 +116,8 @@ export const MessageRpcLive = MessageRpcs.toLayer( const response = yield* db .transaction( Effect.gen(function* () { - yield* MessagePolicy.canUpdate(id) - const updatedMessage = yield* MessageRepo.update({ + yield* messagePolicy.canUpdate(id) + const updatedMessage = yield* messageRepo.update({ id, ...payload, }) @@ -152,7 +156,7 @@ export const MessageRpcLive = MessageRpcs.toLayer( "message.delete": ({ id }) => Effect.gen(function* () { const user = yield* CurrentUser.Context - const existingMessage = yield* MessageRepo.findById(id).pipe( + const existingMessage = yield* messageRepo.findById(id).pipe( withRemapDbErrors("Message", "select"), ) @@ -162,8 +166,8 @@ export const MessageRpcLive = MessageRpcs.toLayer( const response = yield* db .transaction( Effect.gen(function* () { - yield* MessagePolicy.canDelete(id) - yield* MessageRepo.deleteById(id) + yield* messagePolicy.canDelete(id) + yield* messageRepo.deleteById(id) if (Option.isSome(existingMessage)) { yield* outboxRepo.insert({ diff --git a/apps/backend/src/rpc/handlers/notifications.ts b/apps/backend/src/rpc/handlers/notifications.ts index c1d68b6d2..8e7ccbb73 100644 --- a/apps/backend/src/rpc/handlers/notifications.ts +++ b/apps/backend/src/rpc/handlers/notifications.ts @@ -22,14 +22,18 @@ import { NotificationPolicy } from "../../policies/notification-policy" export const NotificationRpcLive = NotificationRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database + const notificationPolicy = yield* NotificationPolicy + const channelRepo = yield* ChannelRepo + const organizationMemberRepo = yield* OrganizationMemberRepo + const notificationRepo = yield* NotificationRepo return { "notification.create": (payload) => db .transaction( Effect.gen(function* () { - yield* NotificationPolicy.canCreate(payload.memberId) - const createdNotification = yield* NotificationRepo.insert({ + yield* notificationPolicy.canCreate(payload.memberId) + const createdNotification = yield* notificationRepo.insert({ ...payload, }).pipe(Effect.map((res) => res[0]!)) @@ -47,8 +51,8 @@ export const NotificationRpcLive = NotificationRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* NotificationPolicy.canUpdate(id) - const updatedNotification = yield* NotificationRepo.update({ + yield* notificationPolicy.canUpdate(id) + const updatedNotification = yield* notificationRepo.update({ id, ...payload, }) @@ -67,8 +71,8 @@ export const NotificationRpcLive = NotificationRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* NotificationPolicy.canDelete(id) - yield* NotificationRepo.deleteById(id) + yield* notificationPolicy.canDelete(id) + yield* notificationRepo.deleteById(id) const txid = yield* generateTransactionId() @@ -88,7 +92,7 @@ export const NotificationRpcLive = NotificationRpcs.toLayer( const user = yield* CurrentUser.Context // Get the channel to find the organization (system operation) - const channelOption = yield* ChannelRepo.findById(channelId).pipe( + const channelOption = yield* channelRepo.findById(channelId).pipe( withRemapDbErrors("Channel", "select"), ) @@ -104,7 +108,7 @@ export const NotificationRpcLive = NotificationRpcs.toLayer( const channel = channelOption.value // Get the organization member for this user (system operation) - const memberOption = yield* OrganizationMemberRepo.findByOrgAndUser( + const memberOption = yield* organizationMemberRepo.findByOrgAndUser( channel.organizationId, user.id, ).pipe(withRemapDbErrors("OrganizationMember", "select")) @@ -125,7 +129,7 @@ export const NotificationRpcLive = NotificationRpcs.toLayer( const result = yield* db .transaction( Effect.gen(function* () { - const deleted = yield* NotificationRepo.deleteByMessageIds( + const deleted = yield* notificationRepo.deleteByMessageIds( messageIds, member.id, ) diff --git a/apps/backend/src/rpc/handlers/organization-members.ts b/apps/backend/src/rpc/handlers/organization-members.ts index c098f6f5d..0a7b4bcf0 100644 --- a/apps/backend/src/rpc/handlers/organization-members.ts +++ b/apps/backend/src/rpc/handlers/organization-members.ts @@ -23,6 +23,9 @@ import { ChannelAccessSyncService } from "../../services/channel-access-sync" export const OrganizationMemberRpcLive = OrganizationMemberRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database + const organizationMemberPolicy = yield* OrganizationMemberPolicy + const organizationMemberRepo = yield* OrganizationMemberRepo + const channelAccessSync = yield* ChannelAccessSyncService return { "organizationMember.create": (payload) => @@ -31,14 +34,14 @@ export const OrganizationMemberRpcLive = OrganizationMemberRpcs.toLayer( Effect.gen(function* () { const user = yield* CurrentUser.Context - yield* OrganizationMemberPolicy.canCreate(payload.organizationId) - const createdOrganizationMember = yield* OrganizationMemberRepo.insert({ + yield* organizationMemberPolicy.canCreate(payload.organizationId) + const createdOrganizationMember = yield* organizationMemberRepo.insert({ ...payload, userId: user.id, deletedAt: null, }).pipe(Effect.map((res) => res[0]!)) - yield* ChannelAccessSyncService.syncUserInOrganization( + yield* channelAccessSync.syncUserInOrganization( createdOrganizationMember.userId, createdOrganizationMember.organizationId, ) @@ -57,8 +60,8 @@ export const OrganizationMemberRpcLive = OrganizationMemberRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* OrganizationMemberPolicy.canUpdate(id) - const updatedOrganizationMember = yield* OrganizationMemberRepo.update({ + yield* organizationMemberPolicy.canUpdate(id) + const updatedOrganizationMember = yield* organizationMemberRepo.update({ id, ...payload, }) @@ -77,9 +80,9 @@ export const OrganizationMemberRpcLive = OrganizationMemberRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* OrganizationMemberPolicy.canUpdate(id) + yield* organizationMemberPolicy.canUpdate(id) const updatedOrganizationMemberOption = - yield* OrganizationMemberRepo.updateMetadata(id, metadata) + yield* organizationMemberRepo.updateMetadata(id, metadata) const updatedOrganizationMember = yield* Option.match( updatedOrganizationMemberOption, @@ -108,13 +111,13 @@ export const OrganizationMemberRpcLive = OrganizationMemberRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* OrganizationMemberPolicy.canDelete(id) - const deletedMemberOption = yield* OrganizationMemberRepo.findById(id) + yield* organizationMemberPolicy.canDelete(id) + const deletedMemberOption = yield* organizationMemberRepo.findById(id) - yield* OrganizationMemberRepo.deleteById(id) + yield* organizationMemberRepo.deleteById(id) if (Option.isSome(deletedMemberOption)) { - yield* ChannelAccessSyncService.syncUserInOrganization( + yield* channelAccessSync.syncUserInOrganization( deletedMemberOption.value.userId, deletedMemberOption.value.organizationId, ) diff --git a/apps/backend/src/rpc/handlers/organizations.ts b/apps/backend/src/rpc/handlers/organizations.ts index 7ac1878f0..40d95fb44 100644 --- a/apps/backend/src/rpc/handlers/organizations.ts +++ b/apps/backend/src/rpc/handlers/organizations.ts @@ -95,6 +95,13 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database const workos = yield* WorkOS + const organizationRepo = yield* OrganizationRepo + const organizationPolicy = yield* OrganizationPolicy + const channelRepo = yield* ChannelRepo + const channelMemberRepo = yield* ChannelMemberRepo + const organizationMemberRepo = yield* OrganizationMemberRepo + const userRepo = yield* UserRepo + const channelAccessSync = yield* ChannelAccessSyncService return { "organization.create": (payload) => @@ -105,7 +112,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( const currentUser = yield* CurrentUser.Context // Get the user's external ID (WorkOS user ID) - const userOption = yield* UserRepo.findById(currentUser.id).pipe( + const userOption = yield* userRepo.findById(currentUser.id).pipe( Effect.catchTags({ DatabaseError: (err) => Effect.fail( @@ -129,7 +136,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( // Check if slug already exists if (payload.slug) { - const existingOrganization = yield* OrganizationRepo.findBySlug(payload.slug) + const existingOrganization = yield* organizationRepo.findBySlug(payload.slug) if (Option.isSome(existingOrganization)) { return yield* Effect.fail( @@ -142,8 +149,8 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( } // Create organization in local database first - yield* OrganizationPolicy.canCreate() - const createdOrganization = yield* OrganizationRepo.insert({ + yield* organizationPolicy.canCreate() + const createdOrganization = yield* organizationRepo.insert({ name: payload.name, slug: payload.slug, logoUrl: payload.logoUrl, @@ -190,7 +197,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( ), ) - yield* OrganizationMemberRepo.upsertByOrgAndUser({ + yield* organizationMemberRepo.upsertByOrgAndUser({ organizationId: createdOrganization.id, userId: currentUser.id, role: "owner", @@ -201,12 +208,12 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( }) // Setup default channels for the organization - yield* OrganizationRepo.setupDefaultChannels( + yield* organizationRepo.setupDefaultChannels( createdOrganization.id, currentUser.id, ) - yield* ChannelAccessSyncService.syncUserInOrganization( + yield* channelAccessSync.syncUserInOrganization( currentUser.id, createdOrganization.id, ) @@ -230,9 +237,9 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* Effect.logInfo("OrganizationRepo.update", payload) - yield* OrganizationPolicy.canUpdate(id) - const updatedOrganization = yield* OrganizationRepo.update({ + yield* Effect.logInfo("organizationRepo.update", payload) + yield* organizationPolicy.canUpdate(id) + const updatedOrganization = yield* organizationRepo.update({ id, ...payload, }) @@ -256,8 +263,8 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* OrganizationPolicy.canDelete(id) - yield* OrganizationRepo.deleteById(id) + yield* organizationPolicy.canDelete(id) + yield* organizationRepo.deleteById(id) const txid = yield* generateTransactionId() @@ -270,8 +277,8 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* OrganizationPolicy.canUpdate(id) - const updatedOrganization = yield* OrganizationRepo.update({ + yield* organizationPolicy.canUpdate(id) + const updatedOrganization = yield* organizationRepo.update({ id, slug, }) @@ -295,8 +302,8 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* OrganizationPolicy.canUpdate(id) - const updatedOrganization = yield* OrganizationRepo.update({ + yield* organizationPolicy.canUpdate(id) + const updatedOrganization = yield* organizationRepo.update({ id, isPublic, }) @@ -318,7 +325,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( "organization.getBySlugPublic": ({ slug }) => Effect.gen(function* () { - const orgOption = yield* OrganizationRepo.findBySlugIfPublic(slug) + const orgOption = yield* organizationRepo.findBySlugIfPublic(slug) if (Option.isNone(orgOption)) { return null @@ -327,7 +334,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( const org = orgOption.value // Count members for this organization - const memberCount = yield* OrganizationMemberRepo.countByOrganization(org.id) + const memberCount = yield* organizationMemberRepo.countByOrganization(org.id) return { id: org.id, @@ -355,7 +362,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( const currentUser = yield* CurrentUser.Context // Find the organization by slug - const orgOption = yield* OrganizationRepo.findBySlug(slug) + const orgOption = yield* organizationRepo.findBySlug(slug) if (Option.isNone(orgOption)) { return yield* new OrganizationNotFoundError({ @@ -373,7 +380,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( } // Get the user's external ID for WorkOS sync - const userOption = yield* UserRepo.findById(currentUser.id).pipe( + const userOption = yield* userRepo.findById(currentUser.id).pipe( Effect.catchTags({ DatabaseError: (err) => Effect.fail( @@ -408,7 +415,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( ) // Check if user is already a member in local DB - const existingMember = yield* OrganizationMemberRepo.findByOrgAndUser( + const existingMember = yield* organizationMemberRepo.findByOrgAndUser( org.id, currentUser.id, ) @@ -434,7 +441,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( } // Create/ensure membership in local database - yield* OrganizationMemberRepo.upsertByOrgAndUser({ + yield* organizationMemberRepo.upsertByOrgAndUser({ organizationId: org.id, userId: currentUser.id, role: "member", @@ -466,10 +473,10 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( } // Add user to the default "general" channel - const generalChannel = yield* ChannelRepo.findByOrgAndName(org.id, "general") + const generalChannel = yield* channelRepo.findByOrgAndName(org.id, "general") if (Option.isSome(generalChannel)) { - yield* ChannelMemberRepo.insert({ + yield* channelMemberRepo.insert({ channelId: generalChannel.value.id, userId: currentUser.id, isHidden: false, @@ -482,7 +489,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( }) } - yield* ChannelAccessSyncService.syncUserInOrganization(currentUser.id, org.id) + yield* channelAccessSync.syncUserInOrganization(currentUser.id, org.id) const txid = yield* generateTransactionId() @@ -502,7 +509,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( "organization.getAdminPortalLink": ({ id, intent }) => Effect.gen(function* () { // Policy check - only admins/owners can access admin portal - yield* OrganizationPolicy.canUpdate(id) + yield* organizationPolicy.canUpdate(id) // Get the WorkOS organization by our local org ID const workosOrg = yield* workos @@ -551,7 +558,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( "organization.listDomains": ({ id }) => Effect.gen(function* () { // Policy check - only admins/owners can list domains - yield* OrganizationPolicy.canUpdate(id) + yield* organizationPolicy.canUpdate(id) // Get the WorkOS organization by our local org ID const workosOrg = yield* workos @@ -578,7 +585,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( "organization.addDomain": ({ id, domain }) => Effect.gen(function* () { // Policy check - only admins/owners can add domains - yield* OrganizationPolicy.canUpdate(id) + yield* organizationPolicy.canUpdate(id) // Get the WorkOS organization by our local org ID const workosOrg = yield* workos @@ -623,7 +630,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( "organization.removeDomain": ({ id, domainId }) => Effect.gen(function* () { // Policy check - only admins/owners can remove domains - yield* OrganizationPolicy.canUpdate(id) + yield* organizationPolicy.canUpdate(id) // Delete domain from WorkOS yield* workos diff --git a/apps/backend/src/rpc/handlers/pinned-messages.ts b/apps/backend/src/rpc/handlers/pinned-messages.ts index 1b60063a2..c02c840af 100644 --- a/apps/backend/src/rpc/handlers/pinned-messages.ts +++ b/apps/backend/src/rpc/handlers/pinned-messages.ts @@ -22,6 +22,10 @@ import { PinnedMessagePolicy } from "../../policies/pinned-message-policy" export const PinnedMessageRpcLive = PinnedMessageRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database + const messagePolicy = yield* MessagePolicy + const messageRepo = yield* MessageRepo + const pinnedMessagePolicy = yield* PinnedMessagePolicy + const pinnedMessageRepo = yield* PinnedMessageRepo return { "pinnedMessage.create": (payload) => @@ -30,8 +34,8 @@ export const PinnedMessageRpcLive = PinnedMessageRpcs.toLayer( Effect.gen(function* () { const user = yield* CurrentUser.Context - yield* PinnedMessagePolicy.canCreate(payload.channelId) - const createdPinnedMessage = yield* PinnedMessageRepo.insert({ + yield* pinnedMessagePolicy.canCreate(payload.channelId) + const createdPinnedMessage = yield* pinnedMessageRepo.insert({ channelId: payload.channelId, messageId: payload.messageId, pinnedBy: user.id, @@ -52,8 +56,8 @@ export const PinnedMessageRpcLive = PinnedMessageRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* PinnedMessagePolicy.canUpdate(id) - const updatedPinnedMessage = yield* PinnedMessageRepo.update({ + yield* pinnedMessagePolicy.canUpdate(id) + const updatedPinnedMessage = yield* pinnedMessageRepo.update({ id, ...payload, }) @@ -72,8 +76,8 @@ export const PinnedMessageRpcLive = PinnedMessageRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* PinnedMessagePolicy.canDelete(id) - yield* PinnedMessageRepo.deleteById(id) + yield* pinnedMessagePolicy.canDelete(id) + yield* pinnedMessageRepo.deleteById(id) const txid = yield* generateTransactionId() diff --git a/apps/backend/src/rpc/handlers/rss-subscriptions.ts b/apps/backend/src/rpc/handlers/rss-subscriptions.ts index a1056a8bf..84580242d 100644 --- a/apps/backend/src/rpc/handlers/rss-subscriptions.ts +++ b/apps/backend/src/rpc/handlers/rss-subscriptions.ts @@ -22,6 +22,7 @@ export const RssSubscriptionRpcLive = RssSubscriptionRpcs.toLayer( const channelRepo = yield* ChannelRepo const subscriptionRepo = yield* RssSubscriptionRepo const integrationBotService = yield* IntegrationBotService + const rssSubscriptionPolicy = yield* RssSubscriptionPolicy return { "rssSubscription.create": (payload) => @@ -64,7 +65,7 @@ export const RssSubscriptionRpcLive = RssSubscriptionRpcs.toLayer( }), }) - yield* RssSubscriptionPolicy.canCreate(payload.channelId) + yield* rssSubscriptionPolicy.canCreate(payload.channelId) // Create subscription const [subscription] = yield* subscriptionRepo.insert({ @@ -97,7 +98,7 @@ export const RssSubscriptionRpcLive = RssSubscriptionRpcs.toLayer( "rssSubscription.list": ({ channelId }) => Effect.gen(function* () { - yield* RssSubscriptionPolicy.canRead(channelId) + yield* rssSubscriptionPolicy.canRead(channelId) const subscriptions = yield* subscriptionRepo.findByChannel(channelId) return new RssSubscriptionListResponse({ data: subscriptions }) @@ -113,7 +114,7 @@ export const RssSubscriptionRpcLive = RssSubscriptionRpcs.toLayer( const organizationId = user.organizationId - yield* RssSubscriptionPolicy.canReadByOrganization(organizationId) + yield* rssSubscriptionPolicy.canReadByOrganization(organizationId) const subscriptions = yield* subscriptionRepo.findByOrganization(organizationId) return new RssSubscriptionListResponse({ data: subscriptions }) @@ -130,7 +131,7 @@ export const RssSubscriptionRpcLive = RssSubscriptionRpcs.toLayer( ) } - yield* RssSubscriptionPolicy.canUpdate(id) + yield* rssSubscriptionPolicy.canUpdate(id) const [updatedSubscription] = yield* subscriptionRepo.updateSettings(id, { isEnabled: payload.isEnabled, @@ -158,7 +159,7 @@ export const RssSubscriptionRpcLive = RssSubscriptionRpcs.toLayer( ) } - yield* RssSubscriptionPolicy.canDelete(id) + yield* rssSubscriptionPolicy.canDelete(id) yield* subscriptionRepo.softDelete(id) diff --git a/apps/backend/src/rpc/handlers/typing-indicators.ts b/apps/backend/src/rpc/handlers/typing-indicators.ts index d96319ac3..c0bce3451 100644 --- a/apps/backend/src/rpc/handlers/typing-indicators.ts +++ b/apps/backend/src/rpc/handlers/typing-indicators.ts @@ -25,6 +25,8 @@ import { TypingIndicatorPolicy } from "../../policies/typing-indicator-policy" export const TypingIndicatorRpcLive = TypingIndicatorRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database + const typingIndicatorPolicy = yield* TypingIndicatorPolicy + const typingIndicatorRepo = yield* TypingIndicatorRepo return { "typingIndicator.create": (payload) => @@ -38,8 +40,8 @@ export const TypingIndicatorRpcLive = TypingIndicatorRpcs.toLayer( }) // Use upsert to create or update typing indicator - yield* TypingIndicatorPolicy.canCreate(payload.channelId) - const result = yield* TypingIndicatorRepo.upsertByChannelAndMember({ + yield* typingIndicatorPolicy.canCreate(payload.channelId) + const result = yield* typingIndicatorRepo.upsertByChannelAndMember({ channelId: payload.channelId, memberId: payload.memberId, lastTyped: payload.lastTyped ?? Date.now(), @@ -72,8 +74,8 @@ export const TypingIndicatorRpcLive = TypingIndicatorRpcs.toLayer( typingIndicatorId: id, }) - yield* TypingIndicatorPolicy.canUpdate(id) - const typingIndicator = yield* TypingIndicatorRepo.update({ + yield* typingIndicatorPolicy.canUpdate(id) + const typingIndicator = yield* typingIndicatorRepo.update({ ...payload, id, lastTyped: Date.now(), @@ -103,8 +105,8 @@ export const TypingIndicatorRpcLive = TypingIndicatorRpcs.toLayer( }) // First find the typing indicator to return it - yield* TypingIndicatorPolicy.canRead(id) - const existingOption = yield* TypingIndicatorRepo.findById(id) + yield* typingIndicatorPolicy.canRead(id) + const existingOption = yield* typingIndicatorRepo.findById(id) if (Option.isNone(existingOption)) { return yield* Effect.fail( @@ -114,8 +116,8 @@ export const TypingIndicatorRpcLive = TypingIndicatorRpcs.toLayer( const existing = existingOption.value - yield* TypingIndicatorPolicy.canDelete({ id }) - yield* TypingIndicatorRepo.deleteById(id) + yield* typingIndicatorPolicy.canDelete({ id }) + yield* typingIndicatorRepo.deleteById(id) const txid = yield* generateTransactionId() yield* Effect.logDebug("typingIndicator.delete succeeded", { diff --git a/apps/backend/src/rpc/handlers/user-presence-status.ts b/apps/backend/src/rpc/handlers/user-presence-status.ts index 3013c938a..08e5a8350 100644 --- a/apps/backend/src/rpc/handlers/user-presence-status.ts +++ b/apps/backend/src/rpc/handlers/user-presence-status.ts @@ -9,6 +9,8 @@ import { UserPresenceStatusPolicy } from "../../policies/user-presence-status-po export const UserPresenceStatusRpcLive = UserPresenceStatusRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database + const userPresenceStatusPolicy = yield* UserPresenceStatusPolicy + const userPresenceStatusRepo = yield* UserPresenceStatusRepo return { "userPresenceStatus.update": (payload) => @@ -17,13 +19,13 @@ export const UserPresenceStatusRpcLive = UserPresenceStatusRpcs.toLayer( Effect.gen(function* () { const user = yield* CurrentUser.Context - yield* UserPresenceStatusPolicy.canRead() - const existingOption = yield* UserPresenceStatusRepo.findByUserId(user.id) + yield* userPresenceStatusPolicy.canRead() + const existingOption = yield* userPresenceStatusRepo.findByUserId(user.id) const existing = Option.getOrNull(existingOption) const now = new Date() - const updatedStatus = yield* UserPresenceStatusRepo.upsertByUserId({ + const updatedStatus = yield* userPresenceStatusRepo.upsertByUserId({ userId: user.id, status: (payload.status ?? existing?.status ?? "online") as | "online" @@ -71,13 +73,13 @@ export const UserPresenceStatusRpcLive = UserPresenceStatusRpcs.toLayer( Effect.gen(function* () { const user = yield* CurrentUser.Context - yield* UserPresenceStatusPolicy.canUpdate() - const result = yield* UserPresenceStatusRepo.updateHeartbeat(user.id) + yield* userPresenceStatusPolicy.canUpdate() + const result = yield* userPresenceStatusRepo.updateHeartbeat(user.id) // If no record exists, create one with online status if (Option.isNone(result)) { const now = new Date() - yield* UserPresenceStatusRepo.upsertByUserId({ + yield* userPresenceStatusRepo.upsertByUserId({ userId: user.id, status: "online", customMessage: null, @@ -103,14 +105,14 @@ export const UserPresenceStatusRpcLive = UserPresenceStatusRpcs.toLayer( Effect.gen(function* () { const user = yield* CurrentUser.Context - yield* UserPresenceStatusPolicy.canRead() - const existingOption = yield* UserPresenceStatusRepo.findByUserId(user.id) + yield* userPresenceStatusPolicy.canRead() + const existingOption = yield* userPresenceStatusRepo.findByUserId(user.id) const existing = Option.getOrNull(existingOption) const now = new Date() - yield* UserPresenceStatusPolicy.canCreate() - const updatedStatus = yield* UserPresenceStatusRepo.upsertByUserId({ + yield* userPresenceStatusPolicy.canCreate() + const updatedStatus = yield* userPresenceStatusRepo.upsertByUserId({ userId: user.id, status: existing?.status ?? "online", customMessage: null, diff --git a/apps/backend/src/rpc/handlers/users.ts b/apps/backend/src/rpc/handlers/users.ts index 8c06675f7..57086bf83 100644 --- a/apps/backend/src/rpc/handlers/users.ts +++ b/apps/backend/src/rpc/handlers/users.ts @@ -11,6 +11,8 @@ export const UserRpcLive = UserRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database const workos = yield* WorkOS + const userPolicy = yield* UserPolicy + const userRepo = yield* UserRepo return { "user.me": () => CurrentUser.Context, @@ -19,8 +21,8 @@ export const UserRpcLive = UserRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* UserPolicy.canUpdate(id) - const updatedUser = yield* UserRepo.update({ + yield* userPolicy.canUpdate(id) + const updatedUser = yield* userRepo.update({ id, ...payload, }) @@ -58,16 +60,16 @@ export const UserRpcLive = UserRpcs.toLayer( db .transaction( Effect.gen(function* () { - yield* UserPolicy.canRead(id) - const userOption = yield* UserRepo.findById(id) + yield* userPolicy.canRead(id) + const userOption = yield* userRepo.findById(id) const user = yield* Option.match(userOption, { onNone: () => Effect.fail(new UserNotFoundError({ userId: id })), onSome: (user) => Effect.succeed(user), }) - yield* UserPolicy.canDelete(id) - yield* UserRepo.deleteById(id) + yield* userPolicy.canDelete(id) + yield* userRepo.deleteById(id) yield* workos .call((client) => client.userManagement.deleteUser(user.externalId)) @@ -96,8 +98,8 @@ export const UserRpcLive = UserRpcs.toLayer( const currentUser = yield* CurrentUser.Context // Update the current user's isOnboarded flag - yield* UserPolicy.canUpdate(currentUser.id) - const updatedUser = yield* UserRepo.update({ + yield* userPolicy.canUpdate(currentUser.id) + const updatedUser = yield* userRepo.update({ id: currentUser.id, isOnboarded: true, }) @@ -119,8 +121,8 @@ export const UserRpcLive = UserRpcs.toLayer( const currentUser = yield* CurrentUser.Context // Fetch user from database to get externalId - yield* UserPolicy.canRead(currentUser.id) - const userOption = yield* UserRepo.findById(currentUser.id) + yield* userPolicy.canRead(currentUser.id) + const userOption = yield* userRepo.findById(currentUser.id) const user = yield* Option.match(userOption, { onNone: () => @@ -153,8 +155,8 @@ export const UserRpcLive = UserRpcs.toLayer( : null // Update user's avatar in our database - yield* UserPolicy.canUpdate(currentUser.id) - const updatedUser = yield* UserRepo.update({ + yield* userPolicy.canUpdate(currentUser.id) + const updatedUser = yield* userRepo.update({ id: currentUser.id, avatarUrl, }) diff --git a/apps/backend/src/rpc/middleware/scope-injection.ts b/apps/backend/src/rpc/middleware/scope-injection.ts index be8e59708..691852bd2 100644 --- a/apps/backend/src/rpc/middleware/scope-injection.ts +++ b/apps/backend/src/rpc/middleware/scope-injection.ts @@ -1,4 +1,4 @@ -import { Context, Effect, Layer, Option } from "effect" +import { ServiceMap, Effect, Layer, Option } from "effect" import { ScopeInjectionMiddleware } from "@hazel/domain/rpc" import { RequiredScopes } from "@hazel/domain/scopes" import { CurrentRpcScopes } from "@hazel/domain/scopes" @@ -12,7 +12,7 @@ import { CurrentRpcScopes } from "@hazel/domain/scopes" export const ScopeInjectionMiddlewareLive = Layer.succeed( ScopeInjectionMiddleware, ScopeInjectionMiddleware.of(({ rpc, next }) => { - const scopesOption = Context.getOption(rpc.annotations, RequiredScopes) + const scopesOption = ServiceMap.get(rpc.annotations, RequiredScopes) if (Option.isNone(scopesOption)) { return next } diff --git a/apps/backend/src/services/channel-access-sync.ts b/apps/backend/src/services/channel-access-sync.ts index 83195967f..47c2fbd93 100644 --- a/apps/backend/src/services/channel-access-sync.ts +++ b/apps/backend/src/services/channel-access-sync.ts @@ -1,12 +1,12 @@ import { and, eq, isNull, notInArray, schema } from "@hazel/db" import type { ChannelId, ConnectConversationId, OrganizationId, UserId } from "@hazel/schema" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" import { transactionAwareExecute } from "../lib/transaction-aware-execute" export class ChannelAccessSyncService extends ServiceMap.Service()( "ChannelAccessSyncService", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const upsertChannelUsers = Effect.fn("ChannelAccessSyncService.upsertChannelUsers")(function* ( channelId: ChannelId, organizationId: OrganizationId, @@ -393,4 +393,6 @@ export class ChannelAccessSyncService extends ServiceMap.Service export class ChatSyncAttributionReconciler extends ServiceMap.Service()( "ChatSyncAttributionReconciler", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const messageRepo = yield* MessageRepo const userRepo = yield* UserRepo const organizationMemberRepo = yield* OrganizationMemberRepo @@ -128,7 +128,7 @@ export class ChatSyncAttributionReconciler extends ServiceMap.Service() const channelAccessSyncService = yield* ChannelAccessSyncService const providerRegistry = yield* ChatSyncProviderRegistry const discordApiClient = yield* Discord.DiscordApiClient + const discordSyncWorker = yield* DiscordSyncWorker const payloadHash = (value: unknown): string => createHash("sha256").update(JSON.stringify(value)).digest("hex") - const claimReceipt = Effect.fn("DiscordSyncWorker.claimReceipt")(function* (params: { + const claimReceipt = Effect.fn("discordSyncWorker.claimReceipt")(function* (params: { syncConnectionId: SyncConnectionId channelLinkId?: SyncChannelLinkId source: "hazel" | "external" @@ -186,7 +187,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }) }) - const writeReceipt = Effect.fn("DiscordSyncWorker.writeReceipt")(function* (params: { + const writeReceipt = Effect.fn("discordSyncWorker.writeReceipt")(function* (params: { syncConnectionId: SyncConnectionId channelLinkId?: SyncChannelLinkId source: "hazel" | "external" @@ -218,7 +219,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() return `${normalizedBase}/${attachmentId}` } - const getAttachmentPublicUrlBase = Effect.fn("DiscordSyncWorker.getAttachmentPublicUrlBase")( + const getAttachmentPublicUrlBase = Effect.fn("discordSyncWorker.getAttachmentPublicUrlBase")( function* () { const configuredBaseUrl = yield* Config.string("S3_PUBLIC_URL").pipe(Effect.option) if (Option.isNone(configuredBaseUrl) || configuredBaseUrl.value.trim().length === 0) { @@ -233,7 +234,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() ) const listMessageAttachmentsForOutboundSync = Effect.fn( - "DiscordSyncWorker.listMessageAttachmentsForOutboundSync", + "discordSyncWorker.listMessageAttachmentsForOutboundSync", )(function* (hazelMessageId: MessageId) { const rows = yield* db.execute((client) => client @@ -266,7 +267,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() })) }) - const getOrCreateShadowUserId = Effect.fn("DiscordSyncWorker.getOrCreateShadowUserId")( + const getOrCreateShadowUserId = Effect.fn("discordSyncWorker.getOrCreateShadowUserId")( function* (params: { provider: ChatSyncProvider organizationId: OrganizationId @@ -433,7 +434,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() return Option.isSome(webhookConfig) && webhookConfig.value.webhookId === externalWebhookId } - const persistWebhookIdentity = Effect.fn("DiscordSyncWorker.persistWebhookIdentity")(function* ( + const persistWebhookIdentity = Effect.fn("discordSyncWorker.persistWebhookIdentity")(function* ( link: { id: SyncChannelLinkId settings: Record | null @@ -465,7 +466,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() yield* channelLinkRepo.updateSettings(link.id, nextSettings) }) - const ensureDiscordWebhookIdentity = Effect.fn("DiscordSyncWorker.ensureDiscordWebhookIdentity")( + const ensureDiscordWebhookIdentity = Effect.fn("discordSyncWorker.ensureDiscordWebhookIdentity")( function* (params: { provider: ChatSyncProvider link: { @@ -576,7 +577,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() ) const getDiscordWebhookIdentityMessageMetadata = Effect.fn( - "DiscordSyncWorker.getDiscordWebhookIdentityMessageMetadata", + "discordSyncWorker.getDiscordWebhookIdentityMessageMetadata", )(function* (authorId: UserId) { const userOption = yield* userRepo.findById(authorId) if (Option.isNone(userOption)) { @@ -593,7 +594,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() return { username, avatarUrl } }) - const sendDiscordMessageViaWebhook = Effect.fn("DiscordSyncWorker.sendDiscordMessageViaWebhook")( + const sendDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.sendDiscordMessageViaWebhook")( function* (params: { link: { id: SyncChannelLinkId @@ -650,7 +651,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }, ) - const updateDiscordMessageViaWebhook = Effect.fn("DiscordSyncWorker.updateDiscordMessageViaWebhook")( + const updateDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.updateDiscordMessageViaWebhook")( function* (params: { link: { id: SyncChannelLinkId @@ -698,7 +699,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }, ) - const deleteDiscordMessageViaWebhook = Effect.fn("DiscordSyncWorker.deleteDiscordMessageViaWebhook")( + const deleteDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.deleteDiscordMessageViaWebhook")( function* (params: { link: { id: SyncChannelLinkId @@ -744,7 +745,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }, ) - const resolveAuthorUserId = Effect.fn("DiscordSyncWorker.resolveAuthorUserId")(function* (params: { + const resolveAuthorUserId = Effect.fn("discordSyncWorker.resolveAuthorUserId")(function* (params: { provider: ChatSyncProvider organizationId: OrganizationId externalUserId: ExternalUserId @@ -780,7 +781,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() const DISCORD_USER_MENTION_PATTERN = /@\[userId:([^\]]+)\]/g const getDiscordMentionFallbackDisplayName = Effect.fn( - "DiscordSyncWorker.getDiscordMentionFallbackDisplayName", + "discordSyncWorker.getDiscordMentionFallbackDisplayName", )(function* (userId: UserId) { const userOption = yield* userRepo.findById(userId) if (Option.isNone(userOption)) { @@ -802,7 +803,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }) const translateHazelMentionsForDiscord = Effect.fn( - "DiscordSyncWorker.translateHazelMentionsForDiscord", + "discordSyncWorker.translateHazelMentionsForDiscord", )(function* (params: { organizationId: OrganizationId; content: string }) { const userIds = [...params.content.matchAll(DISCORD_USER_MENTION_PATTERN)].map( (match) => match[1] as UserId, @@ -851,7 +852,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() externalMessageId: messageLink.externalMessageId as ExternalMessageId, }) - const resolveExternalMessageId = Effect.fn("DiscordSyncWorker.resolveExternalMessageId")( + const resolveExternalMessageId = Effect.fn("discordSyncWorker.resolveExternalMessageId")( function* (params: { syncConnectionId: SyncConnectionId hazelMessageId: MessageId @@ -900,7 +901,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }, ) - const resolveHazelMessageId = Effect.fn("DiscordSyncWorker.resolveHazelMessageId")( + const resolveHazelMessageId = Effect.fn("discordSyncWorker.resolveHazelMessageId")( function* (params: { syncConnectionId: SyncConnectionId externalMessageId: ExternalMessageId @@ -953,7 +954,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() ) const resolveOrCreateOutboundLinkForMessage = Effect.fn( - "DiscordSyncWorker.resolveOrCreateOutboundLinkForMessage", + "discordSyncWorker.resolveOrCreateOutboundLinkForMessage", )(function* (params: { syncConnectionId: SyncConnectionId provider: ChatSyncProvider @@ -1071,7 +1072,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() return normalizeChannelLinkExternalId(threadLink) }) - const syncHazelMessageToProvider = Effect.fn("DiscordSyncWorker.syncHazelMessageToProvider")( + const syncHazelMessageToProvider = Effect.fn("discordSyncWorker.syncHazelMessageToProvider")( function* ( syncConnectionId: SyncConnectionId, hazelMessageId: MessageId, @@ -1223,7 +1224,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }, ) - const syncConnection = Effect.fn("DiscordSyncWorker.syncConnection")(function* ( + const syncConnection = Effect.fn("discordSyncWorker.syncConnection")(function* ( syncConnectionId: SyncConnectionId, maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, ) { @@ -1299,7 +1300,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }) const syncHazelMessageUpdateToProvider = Effect.fn( - "DiscordSyncWorker.syncHazelMessageUpdateToProvider", + "discordSyncWorker.syncHazelMessageUpdateToProvider", )(function* ( syncConnectionId: SyncConnectionId, hazelMessageId: MessageId, @@ -1405,7 +1406,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }) const syncHazelMessageDeleteToProvider = Effect.fn( - "DiscordSyncWorker.syncHazelMessageDeleteToProvider", + "discordSyncWorker.syncHazelMessageDeleteToProvider", )(function* ( syncConnectionId: SyncConnectionId, hazelMessageId: MessageId, @@ -1501,7 +1502,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }) const syncHazelReactionCreateToProvider = Effect.fn( - "DiscordSyncWorker.syncHazelReactionCreateToProvider", + "discordSyncWorker.syncHazelReactionCreateToProvider", )(function* ( syncConnectionId: SyncConnectionId, hazelReactionId: MessageReactionId, @@ -1588,7 +1589,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }) const syncHazelReactionDeleteToProvider = Effect.fn( - "DiscordSyncWorker.syncHazelReactionDeleteToProvider", + "discordSyncWorker.syncHazelReactionDeleteToProvider", )(function* ( syncConnectionId: SyncConnectionId, payload: { @@ -1698,7 +1699,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() } }) - const getActiveOutboundTargets = Effect.fn("DiscordSyncWorker.getActiveOutboundTargets")(function* ( + const getActiveOutboundTargets = Effect.fn("discordSyncWorker.getActiveOutboundTargets")(function* ( hazelChannelId: ChannelId, provider: ChatSyncProvider, ) { @@ -1732,7 +1733,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }) const syncHazelMessageCreateToAllConnections = Effect.fn( - "DiscordSyncWorker.syncHazelMessageCreateToAllConnections", + "discordSyncWorker.syncHazelMessageCreateToAllConnections", )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { const messageOption = yield* messageRepo.findById(hazelMessageId) if (Option.isNone(messageOption)) { @@ -1768,7 +1769,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }) const syncHazelMessageUpdateToAllConnections = Effect.fn( - "DiscordSyncWorker.syncHazelMessageUpdateToAllConnections", + "discordSyncWorker.syncHazelMessageUpdateToAllConnections", )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { const messageOption = yield* messageRepo.findById(hazelMessageId) if (Option.isNone(messageOption)) { @@ -1804,7 +1805,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }) const syncHazelMessageDeleteToAllConnections = Effect.fn( - "DiscordSyncWorker.syncHazelMessageDeleteToAllConnections", + "discordSyncWorker.syncHazelMessageDeleteToAllConnections", )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { const messageOption = yield* messageRepo.findById(hazelMessageId) if (Option.isNone(messageOption)) { @@ -1840,7 +1841,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }) const syncHazelReactionCreateToAllConnections = Effect.fn( - "DiscordSyncWorker.syncHazelReactionCreateToAllConnections", + "discordSyncWorker.syncHazelReactionCreateToAllConnections", )(function* (provider: ChatSyncProvider, hazelReactionId: MessageReactionId, dedupeKey?: string) { const reactionOption = yield* messageReactionRepo.findById(hazelReactionId) if (Option.isNone(reactionOption)) { @@ -1877,7 +1878,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() }) const syncHazelReactionDeleteToAllConnections = Effect.fn( - "DiscordSyncWorker.syncHazelReactionDeleteToAllConnections", + "discordSyncWorker.syncHazelReactionDeleteToAllConnections", )(function* ( provider: ChatSyncProvider, payload: { @@ -1917,7 +1918,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() return { synced, failed } }) - const syncAllActiveConnections = Effect.fn("DiscordSyncWorker.syncAllActiveConnections")(function* ( + const syncAllActiveConnections = Effect.fn("discordSyncWorker.syncAllActiveConnections")(function* ( provider: ChatSyncProvider, maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, ) { @@ -1935,7 +1936,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() ) }) - const ingestMessageCreate = Effect.fn("DiscordSyncWorker.ingestMessageCreate")(function* ( + const ingestMessageCreate = Effect.fn("discordSyncWorker.ingestMessageCreate")(function* ( payload: ChatSyncIngressMessageCreate, ) { const dedupeKey = payload.dedupeKey ?? `external:message:create:${payload.externalMessageId}` @@ -2129,7 +2130,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() return { status: "created" as const, hazelMessageId: message.id } }) - const ingestMessageUpdate = Effect.fn("DiscordSyncWorker.ingestMessageUpdate")(function* ( + const ingestMessageUpdate = Effect.fn("discordSyncWorker.ingestMessageUpdate")(function* ( payload: ChatSyncIngressMessageUpdate, ) { const dedupeKey = payload.dedupeKey ?? `external:message:update:${payload.externalMessageId}` @@ -2239,7 +2240,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() return { status: "updated" as const, hazelMessageId: messageLink.hazelMessageId } }) - const ingestMessageDelete = Effect.fn("DiscordSyncWorker.ingestMessageDelete")(function* ( + const ingestMessageDelete = Effect.fn("discordSyncWorker.ingestMessageDelete")(function* ( payload: ChatSyncIngressMessageDelete, ) { const dedupeKey = payload.dedupeKey ?? `external:message:delete:${payload.externalMessageId}` @@ -2350,7 +2351,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() return { status: "deleted" as const, hazelMessageId: messageLink.hazelMessageId } }) - const ingestReactionAdd = Effect.fn("DiscordSyncWorker.ingestReactionAdd")(function* ( + const ingestReactionAdd = Effect.fn("discordSyncWorker.ingestReactionAdd")(function* ( payload: ChatSyncIngressReactionAdd, ) { const dedupeKey = @@ -2479,7 +2480,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() return { status: "created" as const, hazelReactionId: reaction.id } }) - const ingestReactionRemove = Effect.fn("DiscordSyncWorker.ingestReactionRemove")(function* ( + const ingestReactionRemove = Effect.fn("discordSyncWorker.ingestReactionRemove")(function* ( payload: ChatSyncIngressReactionRemove, ) { const dedupeKey = @@ -2605,7 +2606,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() return { status: "deleted" as const, hazelReactionId: existingReaction.value.id } }) - const ingestThreadCreate = Effect.fn("DiscordSyncWorker.ingestThreadCreate")(function* ( + const ingestThreadCreate = Effect.fn("discordSyncWorker.ingestThreadCreate")(function* ( payload: ChatSyncIngressThreadCreate, ) { const dedupeKey = payload.dedupeKey ?? `external:thread:create:${payload.externalThreadId}` @@ -2764,7 +2765,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() } }), }) { - static readonly layer = Layer.effect(this, this.effect).pipe( + static readonly layer = Layer.effect(this, this.make).pipe( Layer.provide(ChatSyncConnectionRepo.layer), Layer.provide(ChatSyncChannelLinkRepo.layer), Layer.provide(ChatSyncMessageLinkRepo.layer), diff --git a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts index 892d98035..bddd8c0ca 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts @@ -88,7 +88,7 @@ const isDiscordSnowflake = (value: string): boolean => export class ChatSyncProviderRegistry extends ServiceMap.Service()( "ChatSyncProviderRegistry", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const discordApiClient = yield* Discord.DiscordApiClient const getDiscordToken = Effect.fn("ChatSyncProviderRegistry.getDiscordToken")(function* () { @@ -438,7 +438,7 @@ export class ChatSyncProviderRegistry extends ServiceMap.Service()(" }) const syncHazelMessageUpdateToDiscord = Effect.fn( - "DiscordSyncWorker.syncHazelMessageUpdateToDiscord", + "discordSyncWorker.syncHazelMessageUpdateToDiscord", )(function* ( syncConnectionId: SyncConnectionId, hazelMessageId: MessageId, @@ -75,7 +75,7 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" }) const syncHazelMessageDeleteToDiscord = Effect.fn( - "DiscordSyncWorker.syncHazelMessageDeleteToDiscord", + "discordSyncWorker.syncHazelMessageDeleteToDiscord", )(function* ( syncConnectionId: SyncConnectionId, hazelMessageId: MessageId, @@ -89,7 +89,7 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" }) const syncHazelReactionCreateToDiscord = Effect.fn( - "DiscordSyncWorker.syncHazelReactionCreateToDiscord", + "discordSyncWorker.syncHazelReactionCreateToDiscord", )(function* ( syncConnectionId: SyncConnectionId, hazelReactionId: MessageReactionId, @@ -103,7 +103,7 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" }) const syncHazelReactionDeleteToDiscord = Effect.fn( - "DiscordSyncWorker.syncHazelReactionDeleteToDiscord", + "discordSyncWorker.syncHazelReactionDeleteToDiscord", )(function* ( syncConnectionId: SyncConnectionId, payload: { @@ -122,7 +122,7 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" }) const syncHazelMessageCreateToAllConnections = Effect.fn( - "DiscordSyncWorker.syncHazelMessageCreateToAllConnections", + "discordSyncWorker.syncHazelMessageCreateToAllConnections", )(function* (hazelMessageId: MessageId, dedupeKey?: string) { return yield* coreWorker.syncHazelMessageCreateToAllConnections( "discord", @@ -132,7 +132,7 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" }) const syncHazelMessageUpdateToAllConnections = Effect.fn( - "DiscordSyncWorker.syncHazelMessageUpdateToAllConnections", + "discordSyncWorker.syncHazelMessageUpdateToAllConnections", )(function* (hazelMessageId: MessageId, dedupeKey?: string) { return yield* coreWorker.syncHazelMessageUpdateToAllConnections( "discord", @@ -142,7 +142,7 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" }) const syncHazelMessageDeleteToAllConnections = Effect.fn( - "DiscordSyncWorker.syncHazelMessageDeleteToAllConnections", + "discordSyncWorker.syncHazelMessageDeleteToAllConnections", )(function* (hazelMessageId: MessageId, dedupeKey?: string) { return yield* coreWorker.syncHazelMessageDeleteToAllConnections( "discord", @@ -152,7 +152,7 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" }) const syncHazelReactionCreateToAllConnections = Effect.fn( - "DiscordSyncWorker.syncHazelReactionCreateToAllConnections", + "discordSyncWorker.syncHazelReactionCreateToAllConnections", )(function* (hazelReactionId: MessageReactionId, dedupeKey?: string) { return yield* coreWorker.syncHazelReactionCreateToAllConnections( "discord", @@ -162,7 +162,7 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" }) const syncHazelReactionDeleteToAllConnections = Effect.fn( - "DiscordSyncWorker.syncHazelReactionDeleteToAllConnections", + "discordSyncWorker.syncHazelReactionDeleteToAllConnections", )(function* ( payload: { hazelChannelId: ChannelId diff --git a/apps/backend/src/services/connect-conversation-service.ts b/apps/backend/src/services/connect-conversation-service.ts index eb82bf4fd..4ffab4cd3 100644 --- a/apps/backend/src/services/connect-conversation-service.ts +++ b/apps/backend/src/services/connect-conversation-service.ts @@ -15,7 +15,7 @@ import { OrgResolver } from "./org-resolver" export class ConnectConversationService extends ServiceMap.Service()( "ConnectConversationService", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const channelRepo = yield* ChannelRepo const connectParticipantRepo = yield* ConnectParticipantRepo const connectConversationRepo = yield* ConnectConversationRepo @@ -61,7 +61,7 @@ export class ConnectConversationService extends ServiceMap.Service Database.layer({ diff --git a/apps/backend/src/services/integration-token-service.ts b/apps/backend/src/services/integration-token-service.ts index 9377f950e..a5817a7d3 100644 --- a/apps/backend/src/services/integration-token-service.ts +++ b/apps/backend/src/services/integration-token-service.ts @@ -67,7 +67,7 @@ const refreshOAuthToken = ( export class IntegrationTokenService extends ServiceMap.Service()( "IntegrationTokenService", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const encryption = yield* IntegrationEncryption const tokenRepo = yield* IntegrationTokenRepo const connectionRepo = yield* IntegrationConnectionRepo @@ -439,7 +439,7 @@ export class IntegrationTokenService extends ServiceMap.Service export class MessageOutboxDispatcher extends ServiceMap.Service()( "MessageOutboxDispatcher", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const envVars = yield* EnvVars const database = yield* Database.Database const outboxRepo = yield* MessageOutboxRepo @@ -235,7 +235,7 @@ export class MessageOutboxDispatcher extends ServiceMap.Service()( "MessageSideEffectService", { - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const db = yield* Database.Database const discordSyncWorker = yield* DiscordSyncWorker const clusterUrl = yield* Config.string("CLUSTER_URL").pipe(Effect.orDie) @@ -286,5 +286,5 @@ export class MessageSideEffectService extends ServiceMap.Service()("OrgResolver" */ const resolveGrantedScopes = (role: "owner" | "admin" | "member") => Effect.gen(function* () { - const botScopes = yield* FiberRef.get(CurrentBotScopes) - if (Option.isSome(botScopes)) { - return botScopes.value + const botScopes = yield* Effect.serviceOption(CurrentBotScopes) + if (Option.isSome(botScopes) && Option.isSome(botScopes.value)) { + return botScopes.value.value } return scopesForRole(role) }) diff --git a/apps/bot-gateway/src/index.test.ts b/apps/bot-gateway/src/index.test.ts index 9cc9f9efd..e57036d59 100644 --- a/apps/bot-gateway/src/index.test.ts +++ b/apps/bot-gateway/src/index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "bun:test" -import { ConfigProvider, Effect, Either, Layer, Option, Redacted, Runtime } from "effect" +import { ConfigProvider, Effect, Layer, Option, Redacted, Result, ServiceMap } from "effect" import { createGatewayServer, GatewayStartupError, @@ -40,8 +40,8 @@ const makeHub = () => ), }) as any -const makeTestRuntime = async () => - (await Effect.runPromise(Effect.runtime())) as Runtime.Runtime +const makeTestServices = async () => + (await Effect.runPromise(Effect.services())) as ServiceMap.ServiceMap describe("bot-gateway startup", () => { it("maps config failures to GatewayStartupError", async () => { @@ -49,16 +49,16 @@ describe("bot-gateway startup", () => { Effect.scoped( Layer.build( InstrumentedConfigLive.pipe( - Layer.provide(Layer.setConfigProvider(ConfigProvider.fromMap(new Map()))), + Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({}))), ), - ).pipe(Effect.either), + ).pipe(Effect.result), ), ) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(GatewayStartupError) - expect(result.left.dependency).toBe("config") + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { + expect(result.failure).toBeInstanceOf(GatewayStartupError) + expect((result.failure as GatewayStartupError).dependency).toBe("config") } }) @@ -66,21 +66,21 @@ describe("bot-gateway startup", () => { const result = await Effect.runPromise( Effect.scoped( Layer.build( - instrumentStartupLayer(Layer.fail(new Error("db unavailable")), { + instrumentStartupLayer(Layer.effectDiscard(Effect.fail(new Error("db unavailable"))), { dependency: "database", startMessage: "db start", successMessage: "db ok", failureMessage: "db failed", }), - ).pipe(Effect.either), + ).pipe(Effect.result), ), ) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(GatewayStartupError) - expect(result.left.dependency).toBe("database") - expect(result.left.message).toBe("db failed") + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { + expect(result.failure).toBeInstanceOf(GatewayStartupError) + expect((result.failure as GatewayStartupError).dependency).toBe("database") + expect((result.failure as GatewayStartupError).message).toBe("db failed") } }) @@ -88,20 +88,20 @@ describe("bot-gateway startup", () => { const result = await Effect.runPromise( Effect.scoped( Layer.build( - instrumentStartupLayer(Layer.fail(new Error("redis unavailable")), { + instrumentStartupLayer(Layer.effectDiscard(Effect.fail(new Error("redis unavailable"))), { dependency: "redis", startMessage: "redis start", successMessage: "redis ok", failureMessage: "redis failed", }), - ).pipe(Effect.either), + ).pipe(Effect.result), ), ) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(GatewayStartupError) - expect(result.left.dependency).toBe("redis") + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { + expect(result.failure).toBeInstanceOf(GatewayStartupError) + expect((result.failure as GatewayStartupError).dependency).toBe("redis") } }) @@ -109,47 +109,47 @@ describe("bot-gateway startup", () => { const result = await Effect.runPromise( Effect.scoped( Layer.build( - instrumentStartupLayer(Layer.fail(new Error("tracer unavailable")), { + instrumentStartupLayer(Layer.effectDiscard(Effect.fail(new Error("tracer unavailable"))), { dependency: "tracer", startMessage: "tracer start", successMessage: "tracer ok", failureMessage: "tracer failed", }), - ).pipe(Effect.either), + ).pipe(Effect.result), ), ) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(GatewayStartupError) - expect(result.left.dependency).toBe("tracer") + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { + expect(result.failure).toBeInstanceOf(GatewayStartupError) + expect((result.failure as GatewayStartupError).dependency).toBe("tracer") } }) it("maps server bind failures to GatewayStartupError", async () => { - const runtime = await makeTestRuntime() + const services = await makeTestServices() const result = await Effect.runPromise( Effect.scoped( createGatewayServer({ config: TEST_CONFIG as any, hub: makeHub(), - runtime, + runtime: services, serve: () => { throw new Error("bind failed") }, - }).pipe(Effect.either), + }).pipe(Effect.result), ), ) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(GatewayStartupError) - expect(result.left.dependency).toBe("server") + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { + expect(result.failure).toBeInstanceOf(GatewayStartupError) + expect((result.failure as GatewayStartupError).dependency).toBe("server") } }) it("starts the server with a fake serve function and wires handlers", async () => { - const runtime = await makeTestRuntime() + const services = await makeTestServices() let stopCalls = 0 let servedOptions: any = null @@ -158,7 +158,7 @@ describe("bot-gateway startup", () => { createGatewayServer({ config: TEST_CONFIG as any, hub: makeHub(), - runtime, + runtime: services, serve: (options: any) => { servedOptions = options return { diff --git a/apps/bot-gateway/src/index.ts b/apps/bot-gateway/src/index.ts index 435494dc4..2b0688edd 100644 --- a/apps/bot-gateway/src/index.ts +++ b/apps/bot-gateway/src/index.ts @@ -16,13 +16,11 @@ import { Cause, Config, ConfigProvider, - Context, Deferred, Effect, Layer, Option, Ref, - Runtime, Schema, ServiceMap, } from "effect" @@ -124,20 +122,20 @@ const extractGatewayOp = (payload: string | BufferSource): string | undefined => export class GatewayConfig extends ServiceMap.Service()("GatewayConfig", { make: Effect.gen(function* () { const config = { - port: yield* Config.integer("PORT").pipe(Config.withDefault(DEFAULT_PORT)), + port: yield* Config.int("PORT").pipe(Config.withDefault(DEFAULT_PORT)), isDev: yield* Config.boolean("IS_DEV").pipe(Config.withDefault(false)), databaseUrl: yield* Config.redacted("DATABASE_URL"), durableStreamsUrl: yield* Config.string("DURABLE_STREAMS_URL").pipe( Config.withDefault(DEFAULT_DURABLE_STREAMS_URL), ), durableStreamsToken: yield* Config.option(Config.string("DURABLE_STREAMS_TOKEN")), - heartbeatIntervalMs: yield* Config.integer("GATEWAY_HEARTBEAT_INTERVAL_MS").pipe( + heartbeatIntervalMs: yield* Config.int("GATEWAY_HEARTBEAT_INTERVAL_MS").pipe( Config.withDefault(DEFAULT_HEARTBEAT_INTERVAL_MS), ), - leaseTtlSeconds: yield* Config.integer("GATEWAY_LEASE_TTL_SECONDS").pipe( + leaseTtlSeconds: yield* Config.int("GATEWAY_LEASE_TTL_SECONDS").pipe( Config.withDefault(DEFAULT_LEASE_TTL_SECONDS), ), - batchAckTimeoutMs: yield* Config.integer("GATEWAY_BATCH_ACK_TIMEOUT_MS").pipe( + batchAckTimeoutMs: yield* Config.int("GATEWAY_BATCH_ACK_TIMEOUT_MS").pipe( Config.withDefault(DEFAULT_BATCH_ACK_TIMEOUT_MS), ), } as const @@ -304,7 +302,7 @@ class DurableStreamClient extends ServiceMap.Service()("Dur cause, }), }) - const events = yield* Schema.decodeUnknown(Schema.Array(BotGatewayEnvelope))(raw).pipe( + const events = yield* Schema.decodeUnknownEffect(Schema.Array(BotGatewayEnvelope))(raw).pipe( Effect.mapError( (cause) => new DurableStreamGatewayError({ @@ -336,7 +334,7 @@ class BotGatewayHub extends ServiceMap.Service()("BotGatewayHub", const redis = yield* Redis const durableStreams = yield* DurableStreamClient const config = yield* GatewayConfig - const runtime = (yield* Effect.runtime()) as Runtime.Runtime + const runtime = (yield* Effect.services()) as ServiceMap.ServiceMap const sessionsRef = yield* Ref.make(new Map()) const sendFrame = ( @@ -499,7 +497,7 @@ class BotGatewayHub extends ServiceMap.Service()("BotGatewayHub", yield* annotateReconnectReason("durable_stream_unavailable") yield* Effect.logWarning("Gateway delivery loop failed", { error, sessionId: id }) }).pipe( - Effect.zipRight( + Effect.andThen( Effect.gen(function* () { const session = yield* getSession(id) if (session) { @@ -528,7 +526,7 @@ class BotGatewayHub extends ServiceMap.Service()("BotGatewayHub", sessionId: id, }) }).pipe( - Effect.zipRight( + Effect.andThen( Effect.gen(function* () { const session = yield* getSession(id) if (session) { @@ -613,7 +611,7 @@ class BotGatewayHub extends ServiceMap.Service()("BotGatewayHub", resumeOffset: frame.resumeOffset, }) - Runtime.runFork(runtime)(startDeliveryLoop(id)) + Effect.runForkWith(runtime)(startDeliveryLoop(id)) }) const onOpen = (socket: ServerWebSocket<{ sessionId: string | null }>) => @@ -772,7 +770,7 @@ class BotGatewayHub extends ServiceMap.Service()("BotGatewayHub", static readonly layer = Layer.effect(this, this.make) } -const DatabaseLive = Layer.unwrapEffect( +const DatabaseLive = Layer.unwrap( Effect.gen(function* () { const config = yield* GatewayConfig return Database.layer({ @@ -782,7 +780,7 @@ const DatabaseLive = Layer.unwrapEffect( }), ) -const ConfigProviderLive = Layer.setConfigProvider(ConfigProvider.fromEnv()) +const ConfigProviderLive = ConfigProvider.layer(ConfigProvider.fromEnv()) type StartupDependency = "config" | "database" | "redis" | "tracer" | "server" @@ -799,11 +797,11 @@ export const instrumentStartupLayer = ( readonly dependency: StartupDependency readonly startMessage: string readonly successMessage: string - readonly successLogs?: (context: Context.Context) => Record + readonly successLogs?: (context: ServiceMap.ServiceMap) => Record readonly failureMessage: string }, ) => - Layer.unwrapEffect( + Layer.unwrap( Effect.logInfo(options.startMessage).pipe( Effect.as( layer.pipe( @@ -813,14 +811,18 @@ export const instrumentStartupLayer = ( ? Effect.logInfo(options.successMessage, logs) : Effect.logInfo(options.successMessage) }), - Layer.tapErrorCause((cause) => + Layer.tapCause((cause) => Effect.logError(options.failureMessage, { dependency: options.dependency, cause: Cause.pretty(cause), }), ), - Layer.mapError((error) => - makeStartupError(options.dependency, options.failureMessage, error), + Layer.catchCause((cause) => + Layer.effectDiscard( + Effect.fail( + makeStartupError(options.dependency, options.failureMessage, Cause.squash(cause)), + ), + ), ), ), ), @@ -832,7 +834,7 @@ export const InstrumentedConfigLive = instrumentStartupLayer(GatewayConfig.layer startMessage: "Loading gateway startup config...", successMessage: "Gateway startup config loaded", successLogs: (context) => { - const config = Context.get(context, GatewayConfig) + const config = ServiceMap.get(context, GatewayConfig) return { port: config.port, durableStreamsUrl: config.durableStreamsUrl, @@ -852,7 +854,7 @@ export const InstrumentedDatabaseLive = instrumentStartupLayer( }, ) -export const InstrumentedRedisLive = instrumentStartupLayer(Redis.layer, { +export const InstrumentedRedisLive = instrumentStartupLayer(Redis.Default, { dependency: "redis", startMessage: "Initializing bot gateway Redis layer...", successMessage: "Bot gateway Redis layer initialized", @@ -891,9 +893,9 @@ type GatewayServe = (options: any) => { } export const createGatewayServer = (options: { - readonly config: Context.Tag.Service - readonly hub: Context.Tag.Service - readonly runtime: Runtime.Runtime + readonly config: (typeof GatewayConfig)["Service"] + readonly hub: (typeof BotGatewayHub)["Service"] + readonly runtime: ServiceMap.ServiceMap readonly serve?: GatewayServe }) => Effect.gen(function* () { @@ -964,7 +966,7 @@ export const createGatewayServer = (options: { } if (request.method === "GET" && url.pathname === "/bot-gateway/ws") { - return Runtime.runPromise(runtime)( + return Effect.runPromiseWith(runtime)( Effect.gen(function* () { yield* annotateHttpRequest("/bot-gateway/ws", request.method) yield* Effect.logInfo( @@ -993,14 +995,14 @@ export const createGatewayServer = (options: { } if (request.method === "GET" && url.pathname === "/bot-gateway/stream") { - return Runtime.runPromise(runtime)(handleStreamProxyRequest(request, url)) + return Effect.runPromiseWith(runtime)(handleStreamProxyRequest(request, url)) } return new Response("Not found", { status: 404 }) }, websocket: { open(socket: ServerWebSocket<{ sessionId: string | null }>) { - Runtime.runFork(runtime)( + Effect.runForkWith(runtime)( Effect.gen(function* () { yield* Effect.annotateCurrentSpan("http.route", "/bot-gateway/ws") yield* hub.onOpen(socket) @@ -1015,7 +1017,7 @@ export const createGatewayServer = (options: { message: string | BufferSource, ) { const op = extractGatewayOp(message) - Runtime.runFork(runtime)( + Effect.runForkWith(runtime)( Effect.gen(function* () { yield* Effect.annotateCurrentSpan("http.route", "/bot-gateway/ws") yield* annotateSessionContext({ @@ -1033,7 +1035,7 @@ export const createGatewayServer = (options: { ) }, close(socket: ServerWebSocket<{ sessionId: string | null }>) { - Runtime.runFork(runtime)( + Effect.runForkWith(runtime)( Effect.gen(function* () { yield* Effect.annotateCurrentSpan("http.route", "/bot-gateway/ws") yield* annotateSessionContext({ @@ -1068,7 +1070,7 @@ export const makeProgram = (options?: { Effect.gen(function* () { const config = yield* GatewayConfig const hub = yield* BotGatewayHub - const runtime = (yield* Effect.runtime()) as Runtime.Runtime + const runtime = (yield* Effect.services()) as ServiceMap.ServiceMap yield* createGatewayServer({ config, diff --git a/apps/cluster/src/cron/presence-cleanup-cron.ts b/apps/cluster/src/cron/presence-cleanup-cron.ts index d16e47ac8..3aba0bdcc 100644 --- a/apps/cluster/src/cron/presence-cleanup-cron.ts +++ b/apps/cluster/src/cron/presence-cleanup-cron.ts @@ -5,7 +5,7 @@ import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" // Run every 15 seconds (6-field cron: seconds minutes hours day month weekday) -const every15Seconds = Cron.unsafeParse("*/15 * * * * *") +const every15Seconds = Cron.parseUnsafe("*/15 * * * * *") // Timeout: 45 seconds (3x heartbeat interval of 15s) const HEARTBEAT_TIMEOUT_MS = 45_000 diff --git a/apps/cluster/src/cron/rss-poll-cron.ts b/apps/cluster/src/cron/rss-poll-cron.ts index 46ef37039..bf261b646 100644 --- a/apps/cluster/src/cron/rss-poll-cron.ts +++ b/apps/cluster/src/cron/rss-poll-cron.ts @@ -8,7 +8,7 @@ import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" // Run every 5 minutes -const everyFiveMinutes = Cron.unsafeParse("*/5 * * * *") +const everyFiveMinutes = Cron.parseUnsafe("*/5 * * * *") /** * Cron job that polls RSS feeds due for a refresh. diff --git a/apps/cluster/src/cron/status-expiration-cron.ts b/apps/cluster/src/cron/status-expiration-cron.ts index 711141ebb..4d34ba3bd 100644 --- a/apps/cluster/src/cron/status-expiration-cron.ts +++ b/apps/cluster/src/cron/status-expiration-cron.ts @@ -5,7 +5,7 @@ import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" // Run every minute -const everyMinute = Cron.unsafeParse("* * * * *") +const everyMinute = Cron.parseUnsafe("* * * * *") /** * Cron job that clears expired custom statuses. diff --git a/apps/cluster/src/cron/typing-indicator-cleanup-cron.ts b/apps/cluster/src/cron/typing-indicator-cleanup-cron.ts index dfb7364c7..1037c6bb2 100644 --- a/apps/cluster/src/cron/typing-indicator-cleanup-cron.ts +++ b/apps/cluster/src/cron/typing-indicator-cleanup-cron.ts @@ -4,7 +4,7 @@ import * as Cron from "effect/Cron" import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" -const every5Seconds = Cron.unsafeParse("*/5 * * * * *") +const every5Seconds = Cron.parseUnsafe("*/5 * * * * *") const TYPING_INDICATOR_STALE_MS = 12_000 /** diff --git a/apps/cluster/src/cron/upload-cleanup-cron.ts b/apps/cluster/src/cron/upload-cleanup-cron.ts index 5f6606451..4ebfca675 100644 --- a/apps/cluster/src/cron/upload-cleanup-cron.ts +++ b/apps/cluster/src/cron/upload-cleanup-cron.ts @@ -5,7 +5,7 @@ import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" // Run daily at 3 AM -const dailyAt3AM = Cron.unsafeParse("0 3 * * *") +const dailyAt3AM = Cron.parseUnsafe("0 3 * * *") // Max age for uploads to be considered stale (1 hour) const MAX_AGE_MINUTES = 60 diff --git a/apps/cluster/src/cron/workos-sync-cron.ts b/apps/cluster/src/cron/workos-sync-cron.ts index 4bf932f62..a35b5507b 100644 --- a/apps/cluster/src/cron/workos-sync-cron.ts +++ b/apps/cluster/src/cron/workos-sync-cron.ts @@ -4,14 +4,15 @@ import * as Cron from "effect/Cron" import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" -const workOsCron = Cron.unsafeParse("0 */12 * * *") +const workOsCron = Cron.parseUnsafe("0 */12 * * *") export const WorkOSSyncCronLayer = ClusterCron.make({ name: "WorkOSSync", cron: workOsCron, execute: Effect.gen(function* () { yield* Effect.logDebug("Starting scheduled WorkOS sync...") - const result = yield* WorkOSSync.syncAll + const workOSSync = yield* WorkOSSync + const result = yield* workOSSync.syncAll yield* Effect.annotateCurrentSpan("cron.duration_ms", result.endTime - result.startTime) yield* Effect.annotateCurrentSpan("cron.total_errors", result.totalErrors) yield* Effect.logDebug("WorkOS sync completed", { diff --git a/apps/cluster/src/index.ts b/apps/cluster/src/index.ts index 58bff209f..a2ee0b555 100644 --- a/apps/cluster/src/index.ts +++ b/apps/cluster/src/index.ts @@ -1,6 +1,6 @@ import { ClusterWorkflowEngine } from "effect/unstable/cluster" import { HttpApiBuilder } from "effect/unstable/httpapi" -import { HttpMiddleware, HttpServer } from "effect/unstable/http" +import { HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http" import { BunClusterSocket, BunHttpServer, BunRuntime } from "@effect/platform-bun" import { PgClient } from "@effect/sql-pg" import { WorkflowProxyServer } from "effect/unstable/workflow" @@ -88,23 +88,26 @@ const AllCronJobs = Layer.mergeAll( ).pipe(Layer.provide(WorkflowEngineLayer)) // Workflow API implementation -const WorkflowApiLive = HttpApiBuilder.api(Cluster.WorkflowApi).pipe( +const WorkflowApiLive = HttpApiBuilder.layer(Cluster.WorkflowApi).pipe( Layer.provide(WorkflowProxyServer.layerHttpApi(Cluster.WorkflowApi, "workflows", Cluster.workflows)), Layer.provide(HealthLive), - HttpServer.withLogAddress, ) -// Main server layer with CORS enabled -const ServerLayer = HttpApiBuilder.serve( - HttpMiddleware.cors({ - allowedOrigins: ["http://localhost:3000", "https://app.hazel.sh"], - credentials: true, - }), -).pipe( - Layer.provide(WorkflowApiLive), +// All routes with CORS +const AllRoutes = Layer.mergeAll(WorkflowApiLive).pipe( + Layer.provide( + HttpRouter.cors({ + allowedOrigins: ["http://localhost:3000", "https://app.hazel.sh"], + credentials: true, + }), + ), +) + +// Main server layer +const ServerLayer = HttpRouter.serve(AllRoutes).pipe( Layer.provide(AllWorkflows), Layer.provide(AllCronJobs), - Layer.provide(Logger.pretty), + Layer.provide(Logger.layer([Logger.consolePretty()])), Layer.provide( BunHttpServer.layerConfig( Config.all({ @@ -116,6 +119,6 @@ const ServerLayer = HttpApiBuilder.serve( ), ) -Layer.launch(ServerLayer.pipe(Layer.provide(WorkflowEngineLayer), Layer.provide(TracerLive))).pipe( +ServerLayer.pipe(Layer.provide(WorkflowEngineLayer), Layer.provide(TracerLive), Layer.launch).pipe( BunRuntime.runMain, ) diff --git a/apps/cluster/src/workflows/cleanup-uploads-handler.ts b/apps/cluster/src/workflows/cleanup-uploads-handler.ts index 02a8f3840..1669db2a7 100644 --- a/apps/cluster/src/workflows/cleanup-uploads-handler.ts +++ b/apps/cluster/src/workflows/cleanup-uploads-handler.ts @@ -7,105 +7,41 @@ import { Effect } from "effect" const DEFAULT_MAX_AGE_MINUTES = 10 export const CleanupUploadsWorkflowLayer = Cluster.CleanupUploadsWorkflow.toLayer( - Effect.fn("workflow.CleanupUploads")(function* (payload: Cluster.CleanupUploadsWorkflowPayload) { + Effect.fn("workflow.CleanupUploads")(function* (payload: Cluster.CleanupUploadsWorkflowPayload, _executionId: string) { const maxAgeMinutes = payload.maxAgeMinutes ?? DEFAULT_MAX_AGE_MINUTES yield* Effect.annotateCurrentSpan("workflow.max_age_minutes", maxAgeMinutes) yield* Effect.logDebug(`Starting CleanupUploadsWorkflow (maxAgeMinutes: ${maxAgeMinutes})`) - const staleUploadsResult = yield* Activity.make({ - name: "FindStaleUploads", - success: Cluster.FindStaleUploadsResult, - error: Cluster.FindStaleUploadsError, - execute: Effect.gen(function* () { - const db = yield* Database.Database - - yield* Effect.logDebug( - `Finding attachments in 'uploading' status older than ${maxAgeMinutes} minutes`, - ) - - const cutoffTime = new Date(Date.now() - maxAgeMinutes * 60 * 1000) - - const staleUploads = yield* db - .execute((client) => - client - .select({ - id: schema.attachmentsTable.id, - fileName: schema.attachmentsTable.fileName, - uploadedAt: schema.attachmentsTable.uploadedAt, - }) - .from(schema.attachmentsTable) - .where( - and( - eq(schema.attachmentsTable.status, "uploading"), - lt(schema.attachmentsTable.uploadedAt, cutoffTime), - isNull(schema.attachmentsTable.deletedAt), - ), - ), - ) - .pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new Cluster.FindStaleUploadsError({ - message: "Failed to find stale uploads", - cause: err, - }), - ), - }), - ) + const staleUploadsResult = yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "FindStaleUploads", + success: Cluster.FindStaleUploadsResult, + error: Cluster.FindStaleUploadsError, + execute: Effect.gen(function* () { + const db = yield* Database.Database - const uploads = staleUploads.map((upload) => ({ - id: upload.id, - fileName: upload.fileName, - uploadedAt: upload.uploadedAt, - ageMinutes: Math.floor((Date.now() - upload.uploadedAt.getTime()) / (60 * 1000)), - })) - - yield* Effect.annotateCurrentSpan("activity.stale_count", uploads.length) - yield* Effect.logDebug(`Found ${uploads.length} stale uploads`) - - return { - uploads, - totalCount: uploads.length, - } - }), - }).pipe( - Effect.tapError((err) => - Effect.logError("FindStaleUploads activity failed", { - errorTag: err._tag, - retryable: err.retryable, - }), - ), - ) - - // If no stale uploads found, we're done - if (staleUploadsResult.totalCount === 0) { - yield* Effect.logDebug("No stale uploads found, workflow complete") - return - } - - const markFailedResult = yield* Activity.make({ - name: "MarkUploadsFailed", - success: Cluster.MarkUploadsFailedResult, - error: Cluster.MarkUploadsFailedError, - execute: Effect.gen(function* () { - const db = yield* Database.Database - const failedIds: AttachmentId[] = [] + yield* Effect.logDebug( + `Finding attachments in 'uploading' status older than ${maxAgeMinutes} minutes`, + ) - yield* Effect.logDebug(`Marking ${staleUploadsResult.uploads.length} uploads as failed`) + const cutoffTime = new Date(Date.now() - maxAgeMinutes * 60 * 1000) - for (const upload of staleUploadsResult.uploads) { - yield* db + const staleUploads = yield* db .execute((client) => client - .update(schema.attachmentsTable) - .set({ status: "failed" }) + .select({ + id: schema.attachmentsTable.id, + fileName: schema.attachmentsTable.fileName, + uploadedAt: schema.attachmentsTable.uploadedAt, + }) + .from(schema.attachmentsTable) .where( and( - eq(schema.attachmentsTable.id, upload.id), eq(schema.attachmentsTable.status, "uploading"), + lt(schema.attachmentsTable.uploadedAt, cutoffTime), + isNull(schema.attachmentsTable.deletedAt), ), ), ) @@ -113,29 +49,97 @@ export const CleanupUploadsWorkflowLayer = Cluster.CleanupUploadsWorkflow.toLaye Effect.catchTags({ DatabaseError: (err) => Effect.fail( - new Cluster.MarkUploadsFailedError({ - message: `Failed to mark upload ${upload.id} as failed`, + new Cluster.FindStaleUploadsError({ + message: "Failed to find stale uploads", cause: err, }), ), }), ) - failedIds.push(upload.id) - yield* Effect.logDebug( - `Marked attachment ${upload.id} (${upload.fileName}) as failed (age: ${upload.ageMinutes}min)`, - ) - } + const uploads = staleUploads.map((upload) => ({ + id: upload.id, + fileName: upload.fileName, + uploadedAt: upload.uploadedAt, + ageMinutes: Math.floor((Date.now() - upload.uploadedAt.getTime()) / (60 * 1000)), + })) - yield* Effect.annotateCurrentSpan("activity.marked_count", failedIds.length) + yield* Effect.annotateCurrentSpan("activity.stale_count", uploads.length) + yield* Effect.logDebug(`Found ${uploads.length} stale uploads`) - return { - markedCount: failedIds.length, - failedIds, - } - }), + return { + uploads, + totalCount: uploads.length, + } + }), + }) + }).pipe( + Effect.tapError((err: { readonly _tag: string; readonly retryable?: boolean }) => + Effect.logError("FindStaleUploads activity failed", { + errorTag: err._tag, + retryable: err.retryable, + }), + ), + ) + + // If no stale uploads found, we're done + if (staleUploadsResult.totalCount === 0) { + yield* Effect.logDebug("No stale uploads found, workflow complete") + return + } + + const markFailedResult = yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "MarkUploadsFailed", + success: Cluster.MarkUploadsFailedResult, + error: Cluster.MarkUploadsFailedError, + execute: Effect.gen(function* () { + const db = yield* Database.Database + const failedIds: AttachmentId[] = [] + + yield* Effect.logDebug(`Marking ${staleUploadsResult.uploads.length} uploads as failed`) + + for (const upload of staleUploadsResult.uploads) { + yield* db + .execute((client) => + client + .update(schema.attachmentsTable) + .set({ status: "failed" }) + .where( + and( + eq(schema.attachmentsTable.id, upload.id), + eq(schema.attachmentsTable.status, "uploading"), + ), + ), + ) + .pipe( + Effect.catchTags({ + DatabaseError: (err) => + Effect.fail( + new Cluster.MarkUploadsFailedError({ + message: `Failed to mark upload ${upload.id} as failed`, + cause: err, + }), + ), + }), + ) + + failedIds.push(upload.id) + yield* Effect.logDebug( + `Marked attachment ${upload.id} (${upload.fileName}) as failed (age: ${upload.ageMinutes}min)`, + ) + } + + yield* Effect.annotateCurrentSpan("activity.marked_count", failedIds.length) + + return { + markedCount: failedIds.length, + failedIds, + } + }), + }) }).pipe( - Effect.tapError((err) => + Effect.tapError((err: { readonly _tag: string; readonly retryable?: boolean }) => Effect.logError("MarkUploadsFailed activity failed", { errorTag: err._tag, retryable: err.retryable, diff --git a/apps/cluster/src/workflows/github-installation-handler.ts b/apps/cluster/src/workflows/github-installation-handler.ts index adc50b76c..ebc559e53 100644 --- a/apps/cluster/src/workflows/github-installation-handler.ts +++ b/apps/cluster/src/workflows/github-installation-handler.ts @@ -4,7 +4,7 @@ import { Cluster } from "@hazel/domain" import { Effect } from "effect" export const GitHubInstallationWorkflowLayer = Cluster.GitHubInstallationWorkflow.toLayer( - Effect.fn("workflow.GitHubInstallation")(function* (payload: Cluster.GitHubInstallationWorkflowPayload) { + Effect.fn("workflow.GitHubInstallation")(function* (payload: Cluster.GitHubInstallationWorkflowPayload, _executionId: string) { yield* Effect.annotateCurrentSpan("workflow.action", payload.action) yield* Effect.annotateCurrentSpan("workflow.installation_id", payload.installationId) yield* Effect.annotateCurrentSpan("workflow.account_login", payload.accountLogin) @@ -22,72 +22,74 @@ export const GitHubInstallationWorkflowLayer = Cluster.GitHubInstallationWorkflo } // Activity 1: Find the connection by installation ID - const connectionResult = yield* Activity.make({ - name: "FindConnectionByInstallationId", - success: Cluster.FindConnectionByInstallationResult, - error: Cluster.FindConnectionByInstallationError, - execute: Effect.gen(function* () { - const db = yield* Database.Database - - yield* Effect.logDebug(`Querying connection for installation ID ${payload.installationId}`) - - // Query for a connection with matching installationId in metadata - const connections = yield* db - .execute((client) => - client - .select({ - id: schema.integrationConnectionsTable.id, - organizationId: schema.integrationConnectionsTable.organizationId, - status: schema.integrationConnectionsTable.status, - externalAccountName: schema.integrationConnectionsTable.externalAccountName, - }) - .from(schema.integrationConnectionsTable) - .where( - and( - eq(schema.integrationConnectionsTable.provider, "github"), - sql`${schema.integrationConnectionsTable.metadata}->>'installationId' = ${String(payload.installationId)}`, - isNull(schema.integrationConnectionsTable.deletedAt), + const connectionResult = yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "FindConnectionByInstallationId", + success: Cluster.FindConnectionByInstallationResult, + error: Cluster.FindConnectionByInstallationError, + execute: Effect.gen(function* () { + const db = yield* Database.Database + + yield* Effect.logDebug(`Querying connection for installation ID ${payload.installationId}`) + + // Query for a connection with matching installationId in metadata + const connections = yield* db + .execute((client) => + client + .select({ + id: schema.integrationConnectionsTable.id, + organizationId: schema.integrationConnectionsTable.organizationId, + status: schema.integrationConnectionsTable.status, + externalAccountName: schema.integrationConnectionsTable.externalAccountName, + }) + .from(schema.integrationConnectionsTable) + .where( + and( + eq(schema.integrationConnectionsTable.provider, "github"), + sql`${schema.integrationConnectionsTable.metadata}->>'installationId' = ${String(payload.installationId)}`, + isNull(schema.integrationConnectionsTable.deletedAt), + ), ), - ), - ) - .pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new Cluster.FindConnectionByInstallationError({ - installationId: payload.installationId, - message: "Failed to query GitHub connection", - cause: err, - }), - ), - }), - ) + ) + .pipe( + Effect.catchTags({ + DatabaseError: (err) => + Effect.fail( + new Cluster.FindConnectionByInstallationError({ + installationId: payload.installationId, + message: "Failed to query GitHub connection", + cause: err, + }), + ), + }), + ) + + if (connections.length === 0) { + yield* Effect.logDebug( + `No connection found for installation ID ${payload.installationId}`, + ) + return { connections: [], totalCount: 0 } + } + + yield* Effect.annotateCurrentSpan("activity.connection_count", connections.length) - if (connections.length === 0) { yield* Effect.logDebug( - `No connection found for installation ID ${payload.installationId}`, + `Found ${connections.length} connection(s) for installation ${payload.installationId}`, ) - return { connections: [], totalCount: 0 } - } - - yield* Effect.annotateCurrentSpan("activity.connection_count", connections.length) - - yield* Effect.logDebug( - `Found ${connections.length} connection(s) for installation ${payload.installationId}`, - ) - - return { - connections: connections.map((connection) => ({ - id: connection.id, - organizationId: connection.organizationId, - status: connection.status, - externalAccountName: connection.externalAccountName, - })), - totalCount: connections.length, - } - }), + + return { + connections: connections.map((connection) => ({ + id: connection.id, + organizationId: connection.organizationId, + status: connection.status, + externalAccountName: connection.externalAccountName, + })), + totalCount: connections.length, + } + }), + }) }).pipe( - Effect.tapError((err) => + Effect.tapError((err: { readonly _tag: string; readonly retryable?: boolean }) => Effect.logError("FindConnectionByInstallationId activity failed", { errorTag: err._tag, retryable: err.retryable, @@ -108,72 +110,74 @@ export const GitHubInstallationWorkflowLayer = Cluster.GitHubInstallationWorkflo payload.action === "deleted" ? "revoked" : payload.action === "suspend" ? "suspended" : "active" // unsuspend // Activity 2: Update the connection status - const updateResult = yield* Activity.make({ - name: "UpdateConnectionStatus", - success: Cluster.UpdateConnectionStatusResult, - error: Cluster.UpdateConnectionStatusError, - execute: Effect.gen(function* () { - const db = yield* Database.Database - - yield* Effect.logDebug( - `Updating ${connectionResult.totalCount} connection(s) status to '${newStatus}' for installation ${payload.installationId}`, - ) - - // For "deleted" action, also set deletedAt - const updateValues = - payload.action === "deleted" - ? { - status: newStatus as "revoked", - deletedAt: new Date(), - updatedAt: new Date(), - } - : { - status: newStatus as "active" | "suspended", - updatedAt: new Date(), - } - - const updated = yield* db - .execute((client) => - client - .update(schema.integrationConnectionsTable) - .set(updateValues) - .where( - and( - eq(schema.integrationConnectionsTable.provider, "github"), - sql`${schema.integrationConnectionsTable.metadata}->>'installationId' = ${String(payload.installationId)}`, - isNull(schema.integrationConnectionsTable.deletedAt), - ), - ) - .returning({ id: schema.integrationConnectionsTable.id }), - ) - .pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new Cluster.UpdateConnectionStatusError({ - installationId: payload.installationId, - message: "Failed to update connection status", - cause: err, - }), - ), - }), + const updateResult = yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "UpdateConnectionStatus", + success: Cluster.UpdateConnectionStatusResult, + error: Cluster.UpdateConnectionStatusError, + execute: Effect.gen(function* () { + const db = yield* Database.Database + + yield* Effect.logDebug( + `Updating ${connectionResult.totalCount} connection(s) status to '${newStatus}' for installation ${payload.installationId}`, ) - yield* Effect.annotateCurrentSpan("activity.new_status", newStatus) - yield* Effect.annotateCurrentSpan("activity.updated_count", updated.length) + // For "deleted" action, also set deletedAt + const updateValues = + payload.action === "deleted" + ? { + status: newStatus as "revoked", + deletedAt: new Date(), + updatedAt: new Date(), + } + : { + status: newStatus as "active" | "suspended", + updatedAt: new Date(), + } + + const updated = yield* db + .execute((client) => + client + .update(schema.integrationConnectionsTable) + .set(updateValues) + .where( + and( + eq(schema.integrationConnectionsTable.provider, "github"), + sql`${schema.integrationConnectionsTable.metadata}->>'installationId' = ${String(payload.installationId)}`, + isNull(schema.integrationConnectionsTable.deletedAt), + ), + ) + .returning({ id: schema.integrationConnectionsTable.id }), + ) + .pipe( + Effect.catchTags({ + DatabaseError: (err) => + Effect.fail( + new Cluster.UpdateConnectionStatusError({ + installationId: payload.installationId, + message: "Failed to update connection status", + cause: err, + }), + ), + }), + ) + + yield* Effect.annotateCurrentSpan("activity.new_status", newStatus) + yield* Effect.annotateCurrentSpan("activity.updated_count", updated.length) - yield* Effect.logDebug( - `Successfully updated ${updated.length} connection(s) for installation ${payload.installationId} to '${newStatus}'`, - ) + yield* Effect.logDebug( + `Successfully updated ${updated.length} connection(s) for installation ${payload.installationId} to '${newStatus}'`, + ) - return { - updatedCount: updated.length, - connectionIds: updated.map((u) => u.id), - newStatus, - } - }), + return { + updatedCount: updated.length, + connectionIds: updated.map((u) => u.id), + newStatus, + } + }), + }) }).pipe( - Effect.tapError((err) => + Effect.tapError((err: { readonly _tag: string; readonly retryable?: boolean }) => Effect.logError("UpdateConnectionStatus activity failed", { errorTag: err._tag, retryable: err.retryable, diff --git a/apps/cluster/src/workflows/github-webhook-handler.ts b/apps/cluster/src/workflows/github-webhook-handler.ts index c40d2e8eb..bca37c12e 100644 --- a/apps/cluster/src/workflows/github-webhook-handler.ts +++ b/apps/cluster/src/workflows/github-webhook-handler.ts @@ -7,7 +7,7 @@ import { Effect, Option, Schema } from "effect" import { BotUserService } from "../services/bot-user-service.ts" export const GitHubWebhookWorkflowLayer = Cluster.GitHubWebhookWorkflow.toLayer( - Effect.fn("workflow.GitHubWebhook")(function* (payload: Cluster.GitHubWebhookWorkflowPayload) { + Effect.fn("workflow.GitHubWebhook")(function* (payload: Cluster.GitHubWebhookWorkflowPayload, _executionId: string) { yield* Effect.annotateCurrentSpan("workflow.event_type", payload.eventType) yield* Effect.annotateCurrentSpan("workflow.repository", payload.repositoryFullName) yield* Effect.annotateCurrentSpan("workflow.repository_id", payload.repositoryId) @@ -24,64 +24,66 @@ export const GitHubWebhookWorkflowLayer = Cluster.GitHubWebhookWorkflow.toLayer( } // Activity 1: Get all subscriptions for this repository - const subscriptionsResult = yield* Activity.make({ - name: "GetGitHubSubscriptions", - success: Cluster.GetGitHubSubscriptionsResult, - error: Cluster.GetGitHubSubscriptionsError, - execute: Effect.gen(function* () { - const db = yield* Database.Database - - yield* Effect.logDebug(`Querying subscriptions for repository ${payload.repositoryId}`) - - const subscriptions = yield* db - .execute((client) => - client - .select({ - id: schema.githubSubscriptionsTable.id, - channelId: schema.githubSubscriptionsTable.channelId, - enabledEvents: schema.githubSubscriptionsTable.enabledEvents, - branchFilter: schema.githubSubscriptionsTable.branchFilter, - }) - .from(schema.githubSubscriptionsTable) - .where( - and( - eq(schema.githubSubscriptionsTable.repositoryId, payload.repositoryId), - eq(schema.githubSubscriptionsTable.isEnabled, true), - isNull(schema.githubSubscriptionsTable.deletedAt), - ), - ), - ) - .pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new Cluster.GetGitHubSubscriptionsError({ - repositoryId: payload.repositoryId, - message: "Failed to query GitHub subscriptions", - cause: err, - }), + const subscriptionsResult = yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "GetGitHubSubscriptions", + success: Cluster.GetGitHubSubscriptionsResult, + error: Cluster.GetGitHubSubscriptionsError, + execute: Effect.gen(function* () { + const db = yield* Database.Database + + yield* Effect.logDebug(`Querying subscriptions for repository ${payload.repositoryId}`) + + const subscriptions = yield* db + .execute((client) => + client + .select({ + id: schema.githubSubscriptionsTable.id, + channelId: schema.githubSubscriptionsTable.channelId, + enabledEvents: schema.githubSubscriptionsTable.enabledEvents, + branchFilter: schema.githubSubscriptionsTable.branchFilter, + }) + .from(schema.githubSubscriptionsTable) + .where( + and( + eq(schema.githubSubscriptionsTable.repositoryId, payload.repositoryId), + eq(schema.githubSubscriptionsTable.isEnabled, true), + isNull(schema.githubSubscriptionsTable.deletedAt), + ), ), - }), + ) + .pipe( + Effect.catchTags({ + DatabaseError: (err) => + Effect.fail( + new Cluster.GetGitHubSubscriptionsError({ + repositoryId: payload.repositoryId, + message: "Failed to query GitHub subscriptions", + cause: err, + }), + ), + }), + ) + + yield* Effect.annotateCurrentSpan("activity.subscription_count", subscriptions.length) + + yield* Effect.logDebug( + `Found ${subscriptions.length} subscriptions for repository ${payload.repositoryId}`, ) - yield* Effect.annotateCurrentSpan("activity.subscription_count", subscriptions.length) - - yield* Effect.logDebug( - `Found ${subscriptions.length} subscriptions for repository ${payload.repositoryId}`, - ) - - return { - subscriptions: subscriptions.map((s) => ({ - id: s.id, - channelId: s.channelId, - enabledEvents: s.enabledEvents as Cluster.GitHubEventType[], - branchFilter: s.branchFilter, - })), - totalCount: subscriptions.length, - } - }), + return { + subscriptions: subscriptions.map((s) => ({ + id: s.id, + channelId: s.channelId, + enabledEvents: s.enabledEvents as Cluster.GitHubEventType[], + branchFilter: s.branchFilter, + })), + totalCount: subscriptions.length, + } + }), + }) }).pipe( - Effect.tapError((err) => + Effect.tapError((err: { readonly _tag: string; readonly retryable?: boolean }) => Effect.logError("GetGitHubSubscriptions activity failed", { errorTag: err._tag, retryable: err.retryable, @@ -94,7 +96,7 @@ export const GitHubWebhookWorkflowLayer = Cluster.GitHubWebhookWorkflow.toLayer( const ref = eventPayload.ref // Filter subscriptions by event type and branch filter - const eligibleSubscriptions = subscriptionsResult.subscriptions.filter((sub) => { + const eligibleSubscriptions = subscriptionsResult.subscriptions.filter((sub: Cluster.GitHubSubscriptionForWorkflow) => { // Check if event type is enabled if (!sub.enabledEvents.includes(internalEventType)) { return false @@ -124,74 +126,76 @@ export const GitHubWebhookWorkflowLayer = Cluster.GitHubWebhookWorkflow.toLayer( } // Activity 2: Create messages in subscribed channels - const messagesResult = yield* Activity.make({ - name: "CreateGitHubMessages", - success: Cluster.CreateGitHubMessagesResult, - error: Schema.Union([Cluster.CreateGitHubMessageError, Cluster.BotUserQueryError]), - execute: Effect.gen(function* () { - const db = yield* Database.Database - const botUserService = yield* BotUserService - const messageIds: MessageId[] = [] - - yield* Effect.logDebug(`Creating messages in ${eligibleSubscriptions.length} channels`) - - // Get the GitHub bot user ID from cache - const botUserOption = yield* botUserService.getGitHubBotUserId() - - if (Option.isNone(botUserOption)) { - yield* Effect.logWarning("GitHub bot user not found, cannot create messages") - return { messageIds: [], messagesCreated: 0 } - } - - const botUserId = botUserOption.value - - // Create a message in each eligible channel - for (const subscription of eligibleSubscriptions) { - const messageResult = yield* db - .execute((client) => - client - .insert(schema.messagesTable) - .values({ - channelId: subscription.channelId, - authorId: botUserId, - content: "", - embeds: [embed], - replyToMessageId: null, - threadChannelId: null, - deletedAt: null, - }) - .returning({ id: schema.messagesTable.id }), - ) - .pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new Cluster.CreateGitHubMessageError({ - channelId: subscription.channelId, - message: "Failed to create GitHub message", - cause: err, - }), - ), - }), - ) + const messagesResult = yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "CreateGitHubMessages", + success: Cluster.CreateGitHubMessagesResult, + error: Schema.Union([Cluster.CreateGitHubMessageError, Cluster.BotUserQueryError]), + execute: Effect.gen(function* () { + const db = yield* Database.Database + const botUserService = yield* BotUserService + const messageIds: MessageId[] = [] + + yield* Effect.logDebug(`Creating messages in ${eligibleSubscriptions.length} channels`) + + // Get the GitHub bot user ID from cache + const botUserOption = yield* botUserService.getGitHubBotUserId() + + if (Option.isNone(botUserOption)) { + yield* Effect.logWarning("GitHub bot user not found, cannot create messages") + return { messageIds: [], messagesCreated: 0 } + } - if (messageResult.length > 0) { - messageIds.push(messageResult[0]!.id) - yield* Effect.logDebug( - `Created message ${messageResult[0]!.id} in channel ${subscription.channelId}`, - ) + const botUserId = botUserOption.value + + // Create a message in each eligible channel + for (const subscription of eligibleSubscriptions) { + const messageResult = yield* db + .execute((client) => + client + .insert(schema.messagesTable) + .values({ + channelId: subscription.channelId, + authorId: botUserId, + content: "", + embeds: [embed], + replyToMessageId: null, + threadChannelId: null, + deletedAt: null, + }) + .returning({ id: schema.messagesTable.id }), + ) + .pipe( + Effect.catchTags({ + DatabaseError: (err) => + Effect.fail( + new Cluster.CreateGitHubMessageError({ + channelId: subscription.channelId, + message: "Failed to create GitHub message", + cause: err, + }), + ), + }), + ) + + if (messageResult.length > 0) { + messageIds.push(messageResult[0]!.id) + yield* Effect.logDebug( + `Created message ${messageResult[0]!.id} in channel ${subscription.channelId}`, + ) + } } - } - yield* Effect.annotateCurrentSpan("activity.messages_created", messageIds.length) + yield* Effect.annotateCurrentSpan("activity.messages_created", messageIds.length) - return { - messageIds, - messagesCreated: messageIds.length, - } - }), + return { + messageIds, + messagesCreated: messageIds.length, + } + }), + }) }).pipe( - Effect.tapError((err) => + Effect.tapError((err: { readonly _tag: string; readonly retryable?: boolean }) => Effect.logError("CreateGitHubMessages activity failed", { errorTag: err._tag, retryable: err.retryable, diff --git a/apps/cluster/src/workflows/message-notification-handler.ts b/apps/cluster/src/workflows/message-notification-handler.ts index ba04c466d..66d675471 100644 --- a/apps/cluster/src/workflows/message-notification-handler.ts +++ b/apps/cluster/src/workflows/message-notification-handler.ts @@ -2,7 +2,7 @@ import { Activity } from "effect/unstable/workflow" import { and, Database, eq, inArray, isNull, ne, or, schema, sql } from "@hazel/db" import { Cluster } from "@hazel/domain" import type { ChannelMemberId, NotificationId, OrganizationMemberId, UserId } from "@hazel/schema" -import { Array, Effect, Option, Schema } from "effect" +import { Array, Effect, Option, Result, Schema } from "effect" interface OrgMemberLookupRow { orgMemberId: OrganizationMemberId @@ -57,6 +57,7 @@ export const buildNotificationInsertRows = ( export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkflow.toLayer( Effect.fn("workflow.MessageNotification")(function* ( payload: Cluster.MessageNotificationWorkflowPayload, + _executionId: string, ) { yield* Effect.annotateCurrentSpan("workflow.message_id", payload.messageId) yield* Effect.annotateCurrentSpan("workflow.channel_id", payload.channelId) @@ -82,17 +83,144 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf ) // Activity 1: Get notification targets based on channel type and mentions - const membersResult = yield* Activity.make({ - name: "GetChannelMembers", - success: Cluster.GetChannelMembersResult, - error: Cluster.GetChannelMembersError, - execute: Effect.gen(function* () { - const db = yield* Database.Database + const membersResult = yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "GetChannelMembers", + success: Cluster.GetChannelMembersResult, + error: Cluster.GetChannelMembersError, + execute: Effect.gen(function* () { + const db = yield* Database.Database + + if (shouldNotifyAll) { + // DM/group or broadcast mention - notify all members (existing logic) + yield* Effect.logDebug(`Querying all channel members for channel ${payload.channelId}`) + + const channelMembers = yield* db + .execute((client) => + client + .select({ + id: schema.channelMembersTable.id, + channelId: schema.channelMembersTable.channelId, + userId: schema.channelMembersTable.userId, + isMuted: schema.channelMembersTable.isMuted, + notificationCount: schema.channelMembersTable.notificationCount, + }) + .from(schema.channelMembersTable) + .leftJoin( + schema.userPresenceStatusTable, + eq( + schema.channelMembersTable.userId, + schema.userPresenceStatusTable.userId, + ), + ) + .where( + and( + eq(schema.channelMembersTable.channelId, payload.channelId), + eq(schema.channelMembersTable.isMuted, false), + ne(schema.channelMembersTable.userId, payload.authorId), + isNull(schema.channelMembersTable.deletedAt), + or( + // No presence record - send notification + isNull(schema.userPresenceStatusTable.userId), + // Has presence record - check suppressNotifications and activeChannel/status + and( + eq( + schema.userPresenceStatusTable.suppressNotifications, + false, + ), + or( + isNull(schema.userPresenceStatusTable.activeChannelId), + ne( + schema.userPresenceStatusTable.activeChannelId, + payload.channelId, + ), + ne(schema.userPresenceStatusTable.status, "online"), + ), + ), + ), + ), + ), + ) + .pipe( + Effect.catchTags({ + DatabaseError: (err) => + Effect.fail( + new Cluster.GetChannelMembersError({ + channelId: payload.channelId, + message: "Failed to query channel members", + cause: err, + }), + ), + }), + ) + + yield* Effect.annotateCurrentSpan("activity.members_count", channelMembers.length) + + yield* Effect.logDebug( + `Found ${channelMembers.length} members to notify (all members mode)`, + ) + + return { + members: channelMembers, + totalCount: channelMembers.length, + } + } + + // Regular channel - only notify mentioned users and reply-to author + yield* Effect.logDebug( + `Smart notification mode: ${mentions.userMentions.length} user mentions, reply to: ${payload.replyToMessageId ?? "none"}`, + ) - if (shouldNotifyAll) { - // DM/group or broadcast mention - notify all members (existing logic) - yield* Effect.logDebug(`Querying all channel members for channel ${payload.channelId}`) + const usersToNotify: UserId[] = [...mentions.userMentions] + + // If this is a reply, get the original message author + if (payload.replyToMessageId) { + const replyToMessage = yield* db + .execute((client) => + client + .select({ authorId: schema.messagesTable.authorId }) + .from(schema.messagesTable) + .where(eq(schema.messagesTable.id, payload.replyToMessageId!)) + .limit(1), + ) + .pipe( + Effect.catchTags({ + DatabaseError: (err) => + Effect.fail( + new Cluster.GetChannelMembersError({ + channelId: payload.channelId, + message: "Failed to query reply-to message author", + cause: err, + }), + ), + }), + ) + + const replyAuthor = Array.head(replyToMessage).pipe( + Option.map((msg) => msg.authorId), + Option.filter((authorId) => authorId !== payload.authorId), + ) + if (Option.isSome(replyAuthor)) { + yield* Effect.logDebug( + `Adding reply-to author ${replyAuthor.value} to notification list`, + ) + usersToNotify.push(replyAuthor.value) + } + } + + // Remove duplicates and the author + const uniqueUsersToNotify = Array.dedupe(usersToNotify).filter( + (userId) => userId !== payload.authorId, + ) + yield* Effect.logDebug(`Unique users to notify: ${uniqueUsersToNotify.length}`) + + if (uniqueUsersToNotify.length === 0) { + yield* Effect.logDebug("No users to notify in smart mode") + return { members: [], totalCount: 0 } + } + + // Query only the members who should be notified const channelMembers = yield* db .execute((client) => client @@ -106,26 +234,20 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf .from(schema.channelMembersTable) .leftJoin( schema.userPresenceStatusTable, - eq( - schema.channelMembersTable.userId, - schema.userPresenceStatusTable.userId, - ), + eq(schema.channelMembersTable.userId, schema.userPresenceStatusTable.userId), ) .where( and( eq(schema.channelMembersTable.channelId, payload.channelId), + inArray(schema.channelMembersTable.userId, uniqueUsersToNotify), eq(schema.channelMembersTable.isMuted, false), - ne(schema.channelMembersTable.userId, payload.authorId), isNull(schema.channelMembersTable.deletedAt), or( // No presence record - send notification isNull(schema.userPresenceStatusTable.userId), // Has presence record - check suppressNotifications and activeChannel/status and( - eq( - schema.userPresenceStatusTable.suppressNotifications, - false, - ), + eq(schema.userPresenceStatusTable.suppressNotifications, false), or( isNull(schema.userPresenceStatusTable.activeChannelId), ne( @@ -145,7 +267,7 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf Effect.fail( new Cluster.GetChannelMembersError({ channelId: payload.channelId, - message: "Failed to query channel members", + message: "Failed to query channel members for mentions", cause: err, }), ), @@ -154,135 +276,16 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf yield* Effect.annotateCurrentSpan("activity.members_count", channelMembers.length) - yield* Effect.logDebug( - `Found ${channelMembers.length} members to notify (all members mode)`, - ) + yield* Effect.logDebug(`Found ${channelMembers.length} members to notify (smart mode)`) return { members: channelMembers, totalCount: channelMembers.length, } - } - - // Regular channel - only notify mentioned users and reply-to author - yield* Effect.logDebug( - `Smart notification mode: ${mentions.userMentions.length} user mentions, reply to: ${payload.replyToMessageId ?? "none"}`, - ) - - const usersToNotify: UserId[] = [...mentions.userMentions] - - // If this is a reply, get the original message author - if (payload.replyToMessageId) { - const replyToMessage = yield* db - .execute((client) => - client - .select({ authorId: schema.messagesTable.authorId }) - .from(schema.messagesTable) - .where(eq(schema.messagesTable.id, payload.replyToMessageId!)) - .limit(1), - ) - .pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new Cluster.GetChannelMembersError({ - channelId: payload.channelId, - message: "Failed to query reply-to message author", - cause: err, - }), - ), - }), - ) - - const replyAuthor = Array.head(replyToMessage).pipe( - Option.map((msg) => msg.authorId), - Option.filter((authorId) => authorId !== payload.authorId), - ) - if (Option.isSome(replyAuthor)) { - yield* Effect.logDebug( - `Adding reply-to author ${replyAuthor.value} to notification list`, - ) - usersToNotify.push(replyAuthor.value) - } - } - - // Remove duplicates and the author - const uniqueUsersToNotify = Array.dedupe(usersToNotify).filter( - (userId) => userId !== payload.authorId, - ) - - yield* Effect.logDebug(`Unique users to notify: ${uniqueUsersToNotify.length}`) - - if (uniqueUsersToNotify.length === 0) { - yield* Effect.logDebug("No users to notify in smart mode") - return { members: [], totalCount: 0 } - } - - // Query only the members who should be notified - const channelMembers = yield* db - .execute((client) => - client - .select({ - id: schema.channelMembersTable.id, - channelId: schema.channelMembersTable.channelId, - userId: schema.channelMembersTable.userId, - isMuted: schema.channelMembersTable.isMuted, - notificationCount: schema.channelMembersTable.notificationCount, - }) - .from(schema.channelMembersTable) - .leftJoin( - schema.userPresenceStatusTable, - eq(schema.channelMembersTable.userId, schema.userPresenceStatusTable.userId), - ) - .where( - and( - eq(schema.channelMembersTable.channelId, payload.channelId), - inArray(schema.channelMembersTable.userId, uniqueUsersToNotify), - eq(schema.channelMembersTable.isMuted, false), - isNull(schema.channelMembersTable.deletedAt), - or( - // No presence record - send notification - isNull(schema.userPresenceStatusTable.userId), - // Has presence record - check suppressNotifications and activeChannel/status - and( - eq(schema.userPresenceStatusTable.suppressNotifications, false), - or( - isNull(schema.userPresenceStatusTable.activeChannelId), - ne( - schema.userPresenceStatusTable.activeChannelId, - payload.channelId, - ), - ne(schema.userPresenceStatusTable.status, "online"), - ), - ), - ), - ), - ), - ) - .pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new Cluster.GetChannelMembersError({ - channelId: payload.channelId, - message: "Failed to query channel members for mentions", - cause: err, - }), - ), - }), - ) - - yield* Effect.annotateCurrentSpan("activity.members_count", channelMembers.length) - - yield* Effect.logDebug(`Found ${channelMembers.length} members to notify (smart mode)`) - - return { - members: channelMembers, - totalCount: channelMembers.length, - } - }), + }), + }) }).pipe( - Effect.tapError((err) => + Effect.tapError((err: { readonly _tag: string; readonly retryable?: boolean }) => Effect.logError("GetChannelMembers activity failed", { errorTag: err._tag, retryable: err.retryable, @@ -297,102 +300,76 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf } // Activity 2: Create notifications for all members - const notificationsResult = yield* Activity.make({ - name: "CreateNotifications", - success: Cluster.CreateNotificationsResult, - error: Schema.Union([Cluster.CreateNotificationError]), - execute: Effect.gen(function* () { - const db = yield* Database.Database - const startedAt = Date.now() - yield* Effect.annotateCurrentSpan("activity.candidate_count", membersResult.members.length) - yield* Effect.logDebug(`Creating notifications for ${membersResult.members.length} members`) - - const userIds = membersResult.members.map((member) => member.userId) - const orgMembers = yield* db - .execute((client) => - client - .select({ - orgMemberId: schema.organizationMembersTable.id, - userId: schema.organizationMembersTable.userId, - }) - .from(schema.organizationMembersTable) - .innerJoin( - schema.channelsTable, - eq( - schema.channelsTable.organizationId, - schema.organizationMembersTable.organizationId, - ), - ) - .where( - and( - eq(schema.channelsTable.id, payload.channelId), - inArray(schema.organizationMembersTable.userId, userIds), - isNull(schema.organizationMembersTable.deletedAt), - ), - ), - ) - .pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new Cluster.CreateNotificationError({ - messageId: payload.messageId, - message: "Failed to query organization members", - cause: err, - }), + const notificationsResult = yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "CreateNotifications", + success: Cluster.CreateNotificationsResult, + error: Schema.Union([Cluster.CreateNotificationError]), + execute: Effect.gen(function* () { + const db = yield* Database.Database + const startedAt = Date.now() + yield* Effect.annotateCurrentSpan("activity.candidate_count", membersResult.members.length) + yield* Effect.logDebug(`Creating notifications for ${membersResult.members.length} members`) + + const userIds = membersResult.members.map((member: Cluster.ChannelMemberForNotification) => member.userId) + const orgMembers = yield* db + .execute((client) => + client + .select({ + orgMemberId: schema.organizationMembersTable.id, + userId: schema.organizationMembersTable.userId, + }) + .from(schema.organizationMembersTable) + .innerJoin( + schema.channelsTable, + eq( + schema.channelsTable.organizationId, + schema.organizationMembersTable.organizationId, + ), + ) + .where( + and( + eq(schema.channelsTable.id, payload.channelId), + inArray(schema.organizationMembersTable.userId, userIds), + isNull(schema.organizationMembersTable.deletedAt), + ), ), - }), - ) - - const orgMemberLookup = buildOrgMemberLookup(orgMembers) - const { values, channelMemberByOrgMember } = buildNotificationInsertRows( - membersResult.members, - orgMemberLookup, - payload, - ) - - if (values.length === 0) { - yield* Effect.logDebug("No valid organization members to notify") - return { notificationIds: [], notifiedCount: 0 } - } - - const insertedNotifications = yield* db - .execute((client) => - client - .insert(schema.notificationsTable) - .values(values) - .onConflictDoNothing() - .returning({ - id: schema.notificationsTable.id, - memberId: schema.notificationsTable.memberId, + ) + .pipe( + Effect.catchTags({ + DatabaseError: (err) => + Effect.fail( + new Cluster.CreateNotificationError({ + messageId: payload.messageId, + message: "Failed to query organization members", + cause: err, + }), + ), }), - ) - .pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new Cluster.CreateNotificationError({ - messageId: payload.messageId, - message: "Failed to insert notification batch", - cause: err, - }), - ), - }), + ) + + const orgMemberLookup = buildOrgMemberLookup(orgMembers) + const { values, channelMemberByOrgMember } = buildNotificationInsertRows( + membersResult.members, + orgMemberLookup, + payload, ) - const insertedChannelMemberIds = Array.filterMap(insertedNotifications, (row) => - Option.fromNullishOr(channelMemberByOrgMember.get(row.memberId)), - ) + if (values.length === 0) { + yield* Effect.logDebug("No valid organization members to notify") + return { notificationIds: [], notifiedCount: 0 } + } - if (insertedChannelMemberIds.length > 0) { - yield* db + const insertedNotifications = yield* db .execute((client) => client - .update(schema.channelMembersTable) - .set({ - notificationCount: sql`${schema.channelMembersTable.notificationCount} + 1`, - }) - .where(inArray(schema.channelMembersTable.id, insertedChannelMemberIds)), + .insert(schema.notificationsTable) + .values(values) + .onConflictDoNothing() + .returning({ + id: schema.notificationsTable.id, + memberId: schema.notificationsTable.memberId, + }), ) .pipe( Effect.catchTags({ @@ -400,31 +377,60 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf Effect.fail( new Cluster.CreateNotificationError({ messageId: payload.messageId, - message: "Failed to increment notification counts", + message: "Failed to insert notification batch", cause: err, }), ), }), ) - } - - const notificationIds = insertedNotifications.map((row) => row.id) as NotificationId[] - yield* Effect.annotateCurrentSpan("activity.eligible_count", values.length) - yield* Effect.annotateCurrentSpan("activity.inserted_count", insertedNotifications.length) - yield* Effect.logDebug("Notification batch completed", { - candidates: membersResult.members.length, - eligible: values.length, - inserted: insertedNotifications.length, - durationMs: Date.now() - startedAt, - }) - - return { - notificationIds, - notifiedCount: notificationIds.length, - } - }), + + const insertedChannelMemberIds = Array.filterMap(insertedNotifications, (row) => { + const memberId = channelMemberByOrgMember.get(row.memberId) + return memberId != null ? Result.succeed(memberId) : Result.failVoid + }) + + if (insertedChannelMemberIds.length > 0) { + yield* db + .execute((client) => + client + .update(schema.channelMembersTable) + .set({ + notificationCount: sql`${schema.channelMembersTable.notificationCount} + 1`, + }) + .where(inArray(schema.channelMembersTable.id, insertedChannelMemberIds)), + ) + .pipe( + Effect.catchTags({ + DatabaseError: (err) => + Effect.fail( + new Cluster.CreateNotificationError({ + messageId: payload.messageId, + message: "Failed to increment notification counts", + cause: err, + }), + ), + }), + ) + } + + const notificationIds = insertedNotifications.map((row) => row.id) as NotificationId[] + yield* Effect.annotateCurrentSpan("activity.eligible_count", values.length) + yield* Effect.annotateCurrentSpan("activity.inserted_count", insertedNotifications.length) + yield* Effect.logDebug("Notification batch completed", { + candidates: membersResult.members.length, + eligible: values.length, + inserted: insertedNotifications.length, + durationMs: Date.now() - startedAt, + }) + + return { + notificationIds, + notifiedCount: notificationIds.length, + } + }), + }) }).pipe( - Effect.tapError((err) => + Effect.tapError((err: { readonly _tag: string; readonly retryable?: boolean }) => Effect.logError("CreateNotifications activity failed", { errorTag: err._tag, retryable: err.retryable, diff --git a/apps/cluster/src/workflows/rss-feed-poll-handler.ts b/apps/cluster/src/workflows/rss-feed-poll-handler.ts index 64156d20d..01146e5a4 100644 --- a/apps/cluster/src/workflows/rss-feed-poll-handler.ts +++ b/apps/cluster/src/workflows/rss-feed-poll-handler.ts @@ -7,7 +7,7 @@ import { Effect, Option, Schema } from "effect" import { BotUserService } from "../services/bot-user-service.ts" export const RssFeedPollWorkflowLayer = Cluster.RssFeedPollWorkflow.toLayer( - Effect.fn("workflow.RssFeedPoll")(function* (payload: Cluster.RssFeedPollWorkflowPayload) { + Effect.fn("workflow.RssFeedPoll")(function* (payload: Cluster.RssFeedPollWorkflowPayload, _executionId: string) { yield* Effect.annotateCurrentSpan("workflow.subscription_id", payload.subscriptionId) yield* Effect.annotateCurrentSpan("workflow.feed_url", payload.feedUrl) yield* Effect.annotateCurrentSpan("workflow.channel_id", payload.channelId) @@ -17,42 +17,44 @@ export const RssFeedPollWorkflowLayer = Cluster.RssFeedPollWorkflow.toLayer( ) // Activity 1: Fetch and parse the RSS feed - const feedResult = yield* Activity.make({ - name: "FetchAndParseFeed", - success: Cluster.FetchRssFeedResult, - error: Cluster.FetchRssFeedError, - execute: Effect.gen(function* () { - yield* Effect.logDebug(`Fetching RSS feed: ${payload.feedUrl}`) + const feedResult = yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "FetchAndParseFeed", + success: Cluster.FetchRssFeedResult, + error: Cluster.FetchRssFeedError, + execute: Effect.gen(function* () { + yield* Effect.logDebug(`Fetching RSS feed: ${payload.feedUrl}`) - const parsedFeed = yield* Effect.tryPromise({ - try: () => Rss.fetchAndParseFeed(payload.feedUrl), - catch: (error) => - new Cluster.FetchRssFeedError({ - subscriptionId: payload.subscriptionId, - feedUrl: payload.feedUrl, - message: error instanceof Error ? error.message : "Failed to fetch RSS feed", - cause: error, - }), - }) + const parsedFeed = yield* Effect.tryPromise({ + try: () => Rss.fetchAndParseFeed(payload.feedUrl), + catch: (error) => + new Cluster.FetchRssFeedError({ + subscriptionId: payload.subscriptionId, + feedUrl: payload.feedUrl, + message: error instanceof Error ? error.message : "Failed to fetch RSS feed", + cause: error, + }), + }) - yield* Effect.annotateCurrentSpan("activity.item_count", parsedFeed.items.length) - yield* Effect.logDebug(`Fetched ${parsedFeed.items.length} items from ${payload.feedUrl}`) + yield* Effect.annotateCurrentSpan("activity.item_count", parsedFeed.items.length) + yield* Effect.logDebug(`Fetched ${parsedFeed.items.length} items from ${payload.feedUrl}`) - return { - feedTitle: parsedFeed.metadata.title, - feedIconUrl: parsedFeed.metadata.iconUrl, - items: parsedFeed.items.map((item) => ({ - guid: item.guid, - title: item.title, - description: item.description, - link: item.link, - pubDate: item.pubDate, - author: item.author, - })), - } - }), + return { + feedTitle: parsedFeed.metadata.title, + feedIconUrl: parsedFeed.metadata.iconUrl, + items: parsedFeed.items.map((item) => ({ + guid: item.guid, + title: item.title, + description: item.description, + link: item.link, + pubDate: item.pubDate, + author: item.author, + })), + } + }), + }) }).pipe( - Effect.tapError((err) => + Effect.tapError((err: { readonly _tag: string }) => Effect.logError("FetchAndParseFeed activity failed", { errorTag: err._tag, feedUrl: payload.feedUrl, @@ -108,63 +110,160 @@ export const RssFeedPollWorkflowLayer = Cluster.RssFeedPollWorkflow.toLayer( } // Activity 2: Filter new items and post messages - const postResult = yield* Activity.make({ - name: "FilterAndPostItems", - success: Cluster.PostRssItemsResult, - error: Schema.Union([Cluster.PostRssItemsError, Cluster.BotUserQueryError]), - execute: Effect.gen(function* () { - const db = yield* Database.Database - const botUserService = yield* BotUserService + const postResult = yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "FilterAndPostItems", + success: Cluster.PostRssItemsResult, + error: Schema.Union([Cluster.PostRssItemsError, Cluster.BotUserQueryError]), + execute: Effect.gen(function* () { + const db = yield* Database.Database + const botUserService = yield* BotUserService + + // Get the RSS bot user ID + const botUserOption = yield* botUserService.getRssBotUserId() + if (Option.isNone(botUserOption)) { + yield* Effect.logWarning("RSS bot user not found, cannot create messages") + return { messageIds: [], messagesCreated: 0, itemGuidsPosted: [] } + } + const botUserId = botUserOption.value + + // Get already posted items for deduplication + const postedItems = yield* db + .execute((client) => + client + .select({ itemGuid: schema.rssPostedItemsTable.itemGuid }) + .from(schema.rssPostedItemsTable) + .where(eq(schema.rssPostedItemsTable.subscriptionId, payload.subscriptionId)), + ) + .pipe( + Effect.catchTags({ + DatabaseError: (err) => + Effect.fail( + new Cluster.PostRssItemsError({ + channelId: payload.channelId, + message: "Failed to query posted items", + cause: err, + }), + ), + }), + ) + + const postedGuids = new Set(postedItems.map((p) => p.itemGuid)) + + // Filter to only new items (GUID dedup + date filter) + const newItems = feedResult.items.filter((item) => { + if (postedGuids.has(item.guid)) return false + // Skip items published before the subscription was created + if (item.pubDate && new Date(item.pubDate).getTime() < payload.subscribedAt) return false + return true + }) + + if (newItems.length === 0) { + yield* Effect.logDebug("No new items to post") + + // Still update last fetched time (non-critical, use orDie) + yield* db + .execute((client) => + client + .update(schema.rssSubscriptionsTable) + .set({ + lastFetchedAt: new Date(), + consecutiveErrors: 0, + lastErrorMessage: null, + lastErrorAt: null, + ...(feedResult.feedTitle && { feedTitle: feedResult.feedTitle }), + ...(feedResult.feedIconUrl && { + feedIconUrl: feedResult.feedIconUrl, + }), + updatedAt: new Date(), + }) + .where(eq(schema.rssSubscriptionsTable.id, payload.subscriptionId)), + ) + .pipe(Effect.orDie) + + return { messageIds: [], messagesCreated: 0, itemGuidsPosted: [] } + } + + // Post up to 5 items per poll cycle (flood protection) + const itemsToPost = newItems.slice(0, 5) + const messageIds: MessageId[] = [] + const itemGuidsPosted: string[] = [] - // Get the RSS bot user ID - const botUserOption = yield* botUserService.getRssBotUserId() - if (Option.isNone(botUserOption)) { - yield* Effect.logWarning("RSS bot user not found, cannot create messages") - return { messageIds: [], messagesCreated: 0, itemGuidsPosted: [] } - } - const botUserId = botUserOption.value + yield* Effect.annotateCurrentSpan("activity.new_item_count", newItems.length) - // Get already posted items for deduplication - const postedItems = yield* db - .execute((client) => - client - .select({ itemGuid: schema.rssPostedItemsTable.itemGuid }) - .from(schema.rssPostedItemsTable) - .where(eq(schema.rssPostedItemsTable.subscriptionId, payload.subscriptionId)), + yield* Effect.logDebug( + `Posting ${itemsToPost.length} new items (of ${newItems.length} total new)`, ) - .pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new Cluster.PostRssItemsError({ + + for (const item of itemsToPost) { + const embed = Rss.buildRssEmbed(item, feedResult.feedTitle) + + const messageResult = yield* db + .execute((client) => + client + .insert(schema.messagesTable) + .values({ channelId: payload.channelId, - message: "Failed to query posted items", - cause: err, - }), - ), - }), - ) + authorId: botUserId, + content: "", + embeds: [embed], + replyToMessageId: null, + threadChannelId: null, + deletedAt: null, + }) + .returning({ id: schema.messagesTable.id }), + ) + .pipe( + Effect.catchTags({ + DatabaseError: (err) => + Effect.fail( + new Cluster.PostRssItemsError({ + channelId: payload.channelId, + message: "Failed to create RSS message", + cause: err, + }), + ), + }), + ) - const postedGuids = new Set(postedItems.map((p) => p.itemGuid)) + if (messageResult.length > 0) { + const messageId = messageResult[0]!.id + messageIds.push(messageId) + itemGuidsPosted.push(item.guid) - // Filter to only new items (GUID dedup + date filter) - const newItems = feedResult.items.filter((item) => { - if (postedGuids.has(item.guid)) return false - // Skip items published before the subscription was created - if (item.pubDate && new Date(item.pubDate).getTime() < payload.subscribedAt) return false - return true - }) + // Record posted item for deduplication (non-critical) + yield* db + .execute((client) => + client.insert(schema.rssPostedItemsTable).values({ + subscriptionId: payload.subscriptionId, + itemGuid: item.guid, + itemUrl: item.link || null, + messageId, + }), + ) + .pipe( + Effect.catch((err) => + Effect.logWarning( + "Failed to record posted item (may cause duplicate on next poll)", + { error: err, itemGuid: item.guid }, + ), + ), + ) - if (newItems.length === 0) { - yield* Effect.logDebug("No new items to post") + yield* Effect.logDebug(`Posted RSS item "${item.title}" as message ${messageId}`) + } + } - // Still update last fetched time (non-critical, use orDie) + // Update subscription state (non-critical, use orDie) + const lastItem = itemsToPost[0] yield* db .execute((client) => client .update(schema.rssSubscriptionsTable) .set({ lastFetchedAt: new Date(), + lastItemGuid: lastItem?.guid ?? null, + lastItemPublishedAt: lastItem?.pubDate ? new Date(lastItem.pubDate) : null, consecutiveErrors: 0, lastErrorMessage: null, lastErrorAt: null, @@ -178,112 +277,17 @@ export const RssFeedPollWorkflowLayer = Cluster.RssFeedPollWorkflow.toLayer( ) .pipe(Effect.orDie) - return { messageIds: [], messagesCreated: 0, itemGuidsPosted: [] } - } - - // Post up to 5 items per poll cycle (flood protection) - const itemsToPost = newItems.slice(0, 5) - const messageIds: MessageId[] = [] - const itemGuidsPosted: string[] = [] - - yield* Effect.annotateCurrentSpan("activity.new_item_count", newItems.length) - - yield* Effect.logDebug( - `Posting ${itemsToPost.length} new items (of ${newItems.length} total new)`, - ) - - for (const item of itemsToPost) { - const embed = Rss.buildRssEmbed(item, feedResult.feedTitle) - - const messageResult = yield* db - .execute((client) => - client - .insert(schema.messagesTable) - .values({ - channelId: payload.channelId, - authorId: botUserId, - content: "", - embeds: [embed], - replyToMessageId: null, - threadChannelId: null, - deletedAt: null, - }) - .returning({ id: schema.messagesTable.id }), - ) - .pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new Cluster.PostRssItemsError({ - channelId: payload.channelId, - message: "Failed to create RSS message", - cause: err, - }), - ), - }), - ) - - if (messageResult.length > 0) { - const messageId = messageResult[0]!.id - messageIds.push(messageId) - itemGuidsPosted.push(item.guid) - - // Record posted item for deduplication (non-critical) - yield* db - .execute((client) => - client.insert(schema.rssPostedItemsTable).values({ - subscriptionId: payload.subscriptionId, - itemGuid: item.guid, - itemUrl: item.link || null, - messageId, - }), - ) - .pipe( - Effect.catch((err) => - Effect.logWarning( - "Failed to record posted item (may cause duplicate on next poll)", - { error: err, itemGuid: item.guid }, - ), - ), - ) + yield* Effect.annotateCurrentSpan("activity.messages_created", messageIds.length) - yield* Effect.logDebug(`Posted RSS item "${item.title}" as message ${messageId}`) + return { + messageIds, + messagesCreated: messageIds.length, + itemGuidsPosted, } - } - - // Update subscription state (non-critical, use orDie) - const lastItem = itemsToPost[0] - yield* db - .execute((client) => - client - .update(schema.rssSubscriptionsTable) - .set({ - lastFetchedAt: new Date(), - lastItemGuid: lastItem?.guid ?? null, - lastItemPublishedAt: lastItem?.pubDate ? new Date(lastItem.pubDate) : null, - consecutiveErrors: 0, - lastErrorMessage: null, - lastErrorAt: null, - ...(feedResult.feedTitle && { feedTitle: feedResult.feedTitle }), - ...(feedResult.feedIconUrl && { - feedIconUrl: feedResult.feedIconUrl, - }), - updatedAt: new Date(), - }) - .where(eq(schema.rssSubscriptionsTable.id, payload.subscriptionId)), - ) - .pipe(Effect.orDie) - - yield* Effect.annotateCurrentSpan("activity.messages_created", messageIds.length) - - return { - messageIds, - messagesCreated: messageIds.length, - itemGuidsPosted, - } - }), + }), + }) }).pipe( - Effect.tapError((err) => + Effect.tapError((err: { readonly _tag: string }) => Effect.logError("FilterAndPostItems activity failed", { errorTag: err._tag, }), diff --git a/apps/cluster/src/workflows/thread-naming-handler.ts b/apps/cluster/src/workflows/thread-naming-handler.ts index c892251d0..dc5bb8b8f 100644 --- a/apps/cluster/src/workflows/thread-naming-handler.ts +++ b/apps/cluster/src/workflows/thread-naming-handler.ts @@ -1,4 +1,4 @@ -import { LanguageModel } from "effect/unstable/ai" +import { AiError, LanguageModel } from "effect/unstable/ai" import { Activity } from "effect/unstable/workflow" import { and, Database, eq, isNull, schema } from "@hazel/db" import { Cluster } from "@hazel/domain" @@ -19,325 +19,307 @@ Thread replies: Generate a concise thread name:` -// Define the workflow error type union for type safety -type ThreadNamingError = - | Cluster.ThreadChannelNotFoundError - | Cluster.OriginalMessageNotFoundError - | Cluster.ThreadContextQueryError - | Cluster.AIProviderUnavailableError - | Cluster.AIRateLimitError - | Cluster.AIResponseParseError - | Cluster.ThreadNameUpdateError - export const ThreadNamingWorkflowLayer = Cluster.ThreadNamingWorkflow.toLayer( - Effect.fn("workflow.ThreadNaming")(function* (payload: Cluster.ThreadNamingWorkflowPayload) { + Effect.fn("workflow.ThreadNaming")(function* (payload: Cluster.ThreadNamingWorkflowPayload, _executionId: string) { yield* Effect.annotateCurrentSpan("workflow.thread_channel_id", payload.threadChannelId) yield* Effect.annotateCurrentSpan("workflow.original_message_id", payload.originalMessageId) yield* Effect.logDebug(`Starting ThreadNamingWorkflow for thread ${payload.threadChannelId}`) // Activity 1: Get thread context from database - const contextResult: Cluster.GetThreadContextResult = yield* Activity.make({ - name: "GetThreadContext", - success: Cluster.GetThreadContextResult, - error: Cluster.ThreadNamingWorkflowError, - execute: Effect.gen(function* () { - const db = yield* Database.Database + const contextResult: Cluster.GetThreadContextResult = yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "GetThreadContext", + success: Cluster.GetThreadContextResult, + error: Cluster.ThreadNamingWorkflowError, + execute: Effect.gen(function* () { + const db = yield* Database.Database - const threadChannel = yield* db - .execute((client) => - client - .select({ - id: schema.channelsTable.id, - name: schema.channelsTable.name, - parentChannelId: schema.channelsTable.parentChannelId, - }) - .from(schema.channelsTable) - .where(eq(schema.channelsTable.id, payload.threadChannelId)) - .limit(1), - ) - .pipe( - Effect.catchTag("DatabaseError", (err) => - Effect.fail( - new Cluster.ThreadContextQueryError({ - threadChannelId: payload.threadChannelId, - operation: "thread", - cause: err, - }), + const threadChannel = yield* db + .execute((client) => + client + .select({ + id: schema.channelsTable.id, + name: schema.channelsTable.name, + parentChannelId: schema.channelsTable.parentChannelId, + }) + .from(schema.channelsTable) + .where(eq(schema.channelsTable.id, payload.threadChannelId)) + .limit(1), + ) + .pipe( + Effect.catchTag("DatabaseError", (err) => + Effect.fail( + new Cluster.ThreadContextQueryError({ + threadChannelId: payload.threadChannelId, + operation: "thread", + cause: err, + }), + ), ), - ), - ) + ) - if (threadChannel.length === 0) { - return yield* Effect.fail( - new Cluster.ThreadChannelNotFoundError({ - threadChannelId: payload.threadChannelId, - }), - ) - } + if (threadChannel.length === 0) { + return yield* Effect.fail( + new Cluster.ThreadChannelNotFoundError({ + threadChannelId: payload.threadChannelId, + }), + ) + } - const thread = threadChannel[0]! + const thread = threadChannel[0]! - // Get original message (the one with threadChannelId pointing to this thread) - const originalMessage = yield* db - .execute((client) => - client - .select({ - id: schema.messagesTable.id, - content: schema.messagesTable.content, - authorId: schema.messagesTable.authorId, - createdAt: schema.messagesTable.createdAt, - firstName: schema.usersTable.firstName, - lastName: schema.usersTable.lastName, - }) - .from(schema.messagesTable) - .innerJoin( - schema.usersTable, - eq(schema.messagesTable.authorId, schema.usersTable.id), - ) - .where(eq(schema.messagesTable.id, payload.originalMessageId)) - .limit(1), - ) - .pipe( - Effect.catchTag("DatabaseError", (err) => - Effect.fail( - new Cluster.ThreadContextQueryError({ - threadChannelId: payload.threadChannelId, - operation: "originalMessage", - cause: err, - }), + // Get original message (the one with threadChannelId pointing to this thread) + const originalMessage = yield* db + .execute((client) => + client + .select({ + id: schema.messagesTable.id, + content: schema.messagesTable.content, + authorId: schema.messagesTable.authorId, + createdAt: schema.messagesTable.createdAt, + firstName: schema.usersTable.firstName, + lastName: schema.usersTable.lastName, + }) + .from(schema.messagesTable) + .innerJoin( + schema.usersTable, + eq(schema.messagesTable.authorId, schema.usersTable.id), + ) + .where(eq(schema.messagesTable.id, payload.originalMessageId)) + .limit(1), + ) + .pipe( + Effect.catchTag("DatabaseError", (err) => + Effect.fail( + new Cluster.ThreadContextQueryError({ + threadChannelId: payload.threadChannelId, + operation: "originalMessage", + cause: err, + }), + ), ), - ), - ) + ) - if (originalMessage.length === 0) { - return yield* Effect.fail( - new Cluster.OriginalMessageNotFoundError({ - threadChannelId: payload.threadChannelId, - messageId: payload.originalMessageId, - }), - ) - } + if (originalMessage.length === 0) { + return yield* Effect.fail( + new Cluster.OriginalMessageNotFoundError({ + threadChannelId: payload.threadChannelId, + messageId: payload.originalMessageId, + }), + ) + } - const orig = originalMessage[0]! + const orig = originalMessage[0]! - // Get thread messages (messages in the thread channel) - const threadMessages = yield* db - .execute((client) => - client - .select({ - id: schema.messagesTable.id, - content: schema.messagesTable.content, - authorId: schema.messagesTable.authorId, - createdAt: schema.messagesTable.createdAt, - firstName: schema.usersTable.firstName, - lastName: schema.usersTable.lastName, - }) - .from(schema.messagesTable) - .innerJoin( - schema.usersTable, - eq(schema.messagesTable.authorId, schema.usersTable.id), - ) - .where( - and( - eq(schema.messagesTable.channelId, payload.threadChannelId), - isNull(schema.messagesTable.deletedAt), + // Get thread messages (messages in the thread channel) + const threadMessages = yield* db + .execute((client) => + client + .select({ + id: schema.messagesTable.id, + content: schema.messagesTable.content, + authorId: schema.messagesTable.authorId, + createdAt: schema.messagesTable.createdAt, + firstName: schema.usersTable.firstName, + lastName: schema.usersTable.lastName, + }) + .from(schema.messagesTable) + .innerJoin( + schema.usersTable, + eq(schema.messagesTable.authorId, schema.usersTable.id), + ) + .where( + and( + eq(schema.messagesTable.channelId, payload.threadChannelId), + isNull(schema.messagesTable.deletedAt), + ), + ) + .orderBy(schema.messagesTable.createdAt) + .limit(10), + ) + .pipe( + Effect.catchTag("DatabaseError", (err) => + Effect.fail( + new Cluster.ThreadContextQueryError({ + threadChannelId: payload.threadChannelId, + operation: "threadMessages", + cause: err, + }), ), - ) - .orderBy(schema.messagesTable.createdAt) - .limit(10), - ) - .pipe( - Effect.catchTag("DatabaseError", (err) => - Effect.fail( - new Cluster.ThreadContextQueryError({ - threadChannelId: payload.threadChannelId, - operation: "threadMessages", - cause: err, - }), ), - ), - ) + ) - yield* Effect.annotateCurrentSpan("activity.thread_message_count", threadMessages.length) + yield* Effect.annotateCurrentSpan("activity.thread_message_count", threadMessages.length) - return { - threadChannelId: payload.threadChannelId, - currentName: thread.name, - originalMessage: { - id: orig.id, - content: orig.content ?? "", - authorId: orig.authorId, - authorName: `${orig.firstName} ${orig.lastName}`.trim(), - createdAt: orig.createdAt.toISOString(), - }, - threadMessages: threadMessages.map((m) => ({ - id: m.id, - content: m.content ?? "", - authorId: m.authorId, - authorName: `${m.firstName} ${m.lastName}`.trim(), - createdAt: m.createdAt.toISOString(), - })), - } - }), + return { + threadChannelId: payload.threadChannelId, + currentName: thread.name, + originalMessage: { + id: orig.id, + content: orig.content ?? "", + authorId: orig.authorId, + authorName: `${orig.firstName} ${orig.lastName}`.trim(), + createdAt: orig.createdAt.toISOString(), + }, + threadMessages: threadMessages.map((m) => ({ + id: m.id, + content: m.content ?? "", + authorId: m.authorId, + authorName: `${m.firstName} ${m.lastName}`.trim(), + createdAt: m.createdAt.toISOString(), + })), + } + }), + }) }).pipe( - Effect.tapError((err) => + Effect.tapError((err: { readonly _tag: string; readonly cause?: unknown }) => Effect.logError("GetThreadContext activity failed", { threadChannelId: payload.threadChannelId, errorTag: err._tag, - cause: "cause" in err ? String(err.cause) : undefined, + cause: err.cause != null ? String(err.cause) : undefined, }), ), ) // Activity 2: Generate thread name using AI - const nameResult: Cluster.GenerateThreadNameResult = yield* Activity.make({ - name: "GenerateThreadName", - success: Cluster.GenerateThreadNameResult, - error: Cluster.ThreadNamingWorkflowError, - execute: Effect.gen(function* () { - // Build the prompt - const threadMessagesText = contextResult.threadMessages - .map((m: Cluster.ThreadMessageContext) => `${m.authorName}: ${m.content}`) - .join("\n") + const nameResult: Cluster.GenerateThreadNameResult = yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "GenerateThreadName", + success: Cluster.GenerateThreadNameResult, + error: Cluster.ThreadNamingWorkflowError, + execute: Effect.gen(function* () { + // Build the prompt + const threadMessagesText = contextResult.threadMessages + .map((m: Cluster.ThreadMessageContext) => `${m.authorName}: ${m.content}`) + .join("\n") - const prompt = NAMING_PROMPT.replace( - "{originalAuthor}", - contextResult.originalMessage.authorName, - ) - .replace("{originalContent}", contextResult.originalMessage.content) - .replace("{threadMessages}", threadMessagesText || "(no replies yet)") + const prompt = NAMING_PROMPT.replace( + "{originalAuthor}", + contextResult.originalMessage.authorName, + ) + .replace("{originalContent}", contextResult.originalMessage.content) + .replace("{threadMessages}", threadMessagesText || "(no replies yet)") - // Call the AI model - const response = yield* LanguageModel.generateText({ - prompt, - }).pipe( - Effect.catchTags({ - HttpRequestError: (err) => - Effect.fail( - new Cluster.AIProviderUnavailableError({ - provider: "openrouter", - cause: err, - }), - ), - HttpResponseError: (err) => - err.response.status === 429 - ? Effect.fail( - new Cluster.AIRateLimitError({ - provider: "openrouter", - }), - ) - : Effect.fail( - new Cluster.AIProviderUnavailableError({ - provider: "openrouter", - cause: err, - }), - ), - MalformedInput: (err) => - Effect.fail( - new Cluster.AIResponseParseError({ - threadChannelId: payload.threadChannelId, - rawResponse: err.description, - }), - ), - MalformedOutput: (err) => - Effect.fail( - new Cluster.AIResponseParseError({ - threadChannelId: payload.threadChannelId, - rawResponse: err.description, - }), - ), - UnknownError: (err) => - Effect.fail( + type AiMappedError = + | Cluster.AIProviderUnavailableError + | Cluster.AIRateLimitError + | Cluster.AIResponseParseError + + // Call the AI model + const response = yield* LanguageModel.generateText({ + prompt, + }).pipe( + Effect.catchTag("AiError", (err: AiError.AiError): Effect.Effect => { + const reason = err.reason + if (reason._tag === "RateLimitError") { + return Effect.fail( + new Cluster.AIRateLimitError({ + provider: "openrouter", + }), + ) + } + if (reason._tag === "InvalidOutputError" || reason._tag === "StructuredOutputError") { + return Effect.fail( + new Cluster.AIResponseParseError({ + threadChannelId: payload.threadChannelId, + rawResponse: reason.message, + }), + ) + } + return Effect.fail( new Cluster.AIProviderUnavailableError({ provider: "openrouter", cause: err, }), - ), - }), - ) + ) + }), + ) - // Clean up the response - let threadName = response.text.trim() - // Remove quotes if present - threadName = threadName.replace(/^["']|["']$/g, "") - // Truncate if too long (max 50 chars) - if (threadName.length > 50) { - threadName = threadName.substring(0, 47) + "..." - } - // Fallback if empty - if (!threadName) { - threadName = "Discussion" - } + // Clean up the response + let threadName = response.text.trim() + // Remove quotes if present + threadName = threadName.replace(/^["']|["']$/g, "") + // Truncate if too long (max 50 chars) + if (threadName.length > 50) { + threadName = threadName.substring(0, 47) + "..." + } + // Fallback if empty + if (!threadName) { + threadName = "Discussion" + } - yield* Effect.annotateCurrentSpan("activity.ai_provider", "openrouter") - yield* Effect.annotateCurrentSpan("activity.generated_name", threadName) + yield* Effect.annotateCurrentSpan("activity.ai_provider", "openrouter") + yield* Effect.annotateCurrentSpan("activity.generated_name", threadName) - yield* Effect.logDebug(`Generated thread name: ${threadName}`) + yield* Effect.logDebug(`Generated thread name: ${threadName}`) - return { threadName } - }), + return { threadName } + }), + }) }).pipe( - Effect.tapError((err) => + Effect.tapError((err: { readonly _tag: string; readonly provider?: string; readonly cause?: unknown }) => Effect.logError("GenerateThreadName activity failed", { threadChannelId: payload.threadChannelId, errorTag: err._tag, - provider: "provider" in err ? err.provider : undefined, - cause: "cause" in err ? String(err.cause) : undefined, + provider: err.provider, + cause: err.cause != null ? String(err.cause) : undefined, }), ), ) // Activity 3: Update thread name in database - yield* Activity.make({ - name: "UpdateThreadName", - success: Cluster.UpdateThreadNameResult, - error: Cluster.ThreadNamingWorkflowError, - execute: Effect.gen(function* () { - const db = yield* Database.Database + yield* Effect.gen(function* () { + return yield* Activity.make({ + name: "UpdateThreadName", + success: Cluster.UpdateThreadNameResult, + error: Cluster.ThreadNamingWorkflowError, + execute: Effect.gen(function* () { + const db = yield* Database.Database - yield* db - .execute((client) => - client - .update(schema.channelsTable) - .set({ - name: nameResult.threadName, - updatedAt: new Date(), - }) - .where(eq(schema.channelsTable.id, payload.threadChannelId)), - ) - .pipe( - Effect.catchTag("DatabaseError", (err) => - Effect.fail( - new Cluster.ThreadNameUpdateError({ - threadChannelId: payload.threadChannelId, - newName: nameResult.threadName, - cause: err, - }), + yield* db + .execute((client) => + client + .update(schema.channelsTable) + .set({ + name: nameResult.threadName, + updatedAt: new Date(), + }) + .where(eq(schema.channelsTable.id, payload.threadChannelId)), + ) + .pipe( + Effect.catchTag("DatabaseError", (err) => + Effect.fail( + new Cluster.ThreadNameUpdateError({ + threadChannelId: payload.threadChannelId, + newName: nameResult.threadName, + cause: err, + }), + ), ), - ), - ) + ) - yield* Effect.annotateCurrentSpan("activity.previous_name", contextResult.currentName ?? "") - yield* Effect.annotateCurrentSpan("activity.new_name", nameResult.threadName) + yield* Effect.annotateCurrentSpan("activity.previous_name", contextResult.currentName ?? "") + yield* Effect.annotateCurrentSpan("activity.new_name", nameResult.threadName) - yield* Effect.logDebug( - `Updated thread ${payload.threadChannelId} name from "${contextResult.currentName}" to "${nameResult.threadName}"`, - ) + yield* Effect.logDebug( + `Updated thread ${payload.threadChannelId} name from "${contextResult.currentName}" to "${nameResult.threadName}"`, + ) - return { - success: true, - previousName: contextResult.currentName, - newName: nameResult.threadName, - } - }), + return { + success: true, + previousName: contextResult.currentName, + newName: nameResult.threadName, + } + }), + }) }).pipe( - Effect.tapError((err) => + Effect.tapError((err: { readonly _tag: string; readonly newName?: string; readonly cause?: unknown }) => Effect.logError("UpdateThreadName activity failed", { threadChannelId: payload.threadChannelId, errorTag: err._tag, - newName: "newName" in err ? err.newName : undefined, - cause: "cause" in err ? String(err.cause) : undefined, + newName: err.newName, + cause: err.cause != null ? String(err.cause) : undefined, }), ), ) diff --git a/apps/electric-proxy/src/cache/access-context-service.ts b/apps/electric-proxy/src/cache/access-context-service.ts index 222120c1e..4e4f6515c 100644 --- a/apps/electric-proxy/src/cache/access-context-service.ts +++ b/apps/electric-proxy/src/cache/access-context-service.ts @@ -1,7 +1,7 @@ import { PersistedCache, Persistence } from "effect/unstable/persistence" import { and, Database, eq, isNull, schema } from "@hazel/db" import type { BotId, ChannelId, UserId } from "@hazel/schema" -import { ServiceMap, Effect, Layer } from "effect" +import { ServiceMap, Effect, Layer, Schema } from "effect" import { AccessContextLookupError, type BotAccessContext, @@ -20,7 +20,10 @@ export interface AccessContextCache { readonly getBotContext: ( botId: BotId, userId: UserId, - ) => Effect.Effect + ) => Effect.Effect< + BotAccessContext, + AccessContextLookupError | Persistence.PersistenceError | Schema.SchemaError + > readonly invalidateBot: (botId: BotId) => Effect.Effect } @@ -32,91 +35,92 @@ export interface AccessContextCache { * Note: Database.Database is intentionally NOT included in dependencies * as it's a global infrastructure layer provided at the application root. */ -export class AccessContextCacheService extends ServiceMap.Service()( +export class AccessContextCacheService extends ServiceMap.Service()( "AccessContextCacheService", -) { - static readonly make = Effect.gen(function* () { - const db = yield* Database.Database + { + make: Effect.gen(function* () { + const db = yield* Database.Database - // Create bot access context cache - const botCache = yield* PersistedCache.make({ - storeId: `${CACHE_STORE_ID}:bot`, + // Create bot access context cache + const botCache = yield* PersistedCache.make({ + storeId: `${CACHE_STORE_ID}:bot`, - lookup: (request: BotAccessContextRequest) => - Effect.gen(function* () { - yield* Effect.annotateCurrentSpan("cache.lookup_performed", true) - yield* Effect.annotateCurrentSpan("cache.result", "miss") - const botId = request.botId as BotId + lookup: (request: BotAccessContextRequest) => + Effect.gen(function* () { + yield* Effect.annotateCurrentSpan("cache.lookup_performed", true) + yield* Effect.annotateCurrentSpan("cache.result", "miss") + const botId = request.botId as BotId - // Query channels in all orgs where the bot is installed. - // Bots are org-level (not channel members), so we join - // bot_installations → channels by organizationId. - const channels = yield* db - .execute((client) => - client - .selectDistinct({ channelId: schema.channelsTable.id }) - .from(schema.botInstallationsTable) - .innerJoin( - schema.channelsTable, - and( - eq( - schema.channelsTable.organizationId, - schema.botInstallationsTable.organizationId, + // Query channels in all orgs where the bot is installed. + // Bots are org-level (not channel members), so we join + // bot_installations → channels by organizationId. + const channels = yield* db + .execute((client) => + client + .selectDistinct({ channelId: schema.channelsTable.id }) + .from(schema.botInstallationsTable) + .innerJoin( + schema.channelsTable, + and( + eq( + schema.channelsTable.organizationId, + schema.botInstallationsTable.organizationId, + ), + isNull(schema.channelsTable.deletedAt), ), - isNull(schema.channelsTable.deletedAt), - ), - ) - .where(eq(schema.botInstallationsTable.botId, botId)), - ) - .pipe( - Effect.catchTag( - "DatabaseError", - (error) => - Effect.fail( - new AccessContextLookupError({ - message: "Failed to query bot's channels", - detail: error.message, - entityId: request.botId, - entityType: "bot", - }), - ), - ), - ) - - const channelIds = channels.map((c: { channelId: ChannelId }) => c.channelId) - yield* Effect.annotateCurrentSpan("cache.result_size", channelIds.length) + ) + .where(eq(schema.botInstallationsTable.botId, botId)), + ) + .pipe( + Effect.catchTag( + "DatabaseError", + (error) => + Effect.fail( + new AccessContextLookupError({ + message: "Failed to query bot's channels", + detail: error.message, + entityId: request.botId, + entityType: "bot", + }), + ), + ), + ) - return { channelIds } - }), + const channelIds = channels.map((c: { channelId: ChannelId }) => c.channelId) + yield* Effect.annotateCurrentSpan("cache.result_size", channelIds.length) - timeToLive: (_exit, _request) => CACHE_TTL, - inMemoryCapacity: IN_MEMORY_CAPACITY, - inMemoryTTL: (_exit, _request) => IN_MEMORY_TTL, - }) + return { channelIds } + }), - return { - getBotContext: Effect.fn("AccessContextCache.getBotContext")(function* ( - botId: BotId, - userId: UserId, - ) { - yield* Effect.annotateCurrentSpan("cache.system", "redis") - yield* Effect.annotateCurrentSpan("cache.name", "electric-proxy:access-context:bot") - yield* Effect.annotateCurrentSpan("cache.operation", "get") - yield* Effect.annotateCurrentSpan("cache.lookup_performed", false) - yield* Effect.annotateCurrentSpan("cache.result", "hit") - const result = yield* botCache.get(new BotAccessContextRequest({ botId, userId })) - return { channelIds: result.channelIds as readonly ChannelId[] } - }), + timeToLive: (_exit, _request) => CACHE_TTL, + inMemoryCapacity: IN_MEMORY_CAPACITY, + inMemoryTTL: (_exit, _request) => IN_MEMORY_TTL, + }) - invalidateBot: Effect.fn("AccessContextCache.invalidateBot")(function* (botId: BotId) { - yield* Effect.annotateCurrentSpan("cache.system", "redis") - yield* Effect.annotateCurrentSpan("cache.name", "electric-proxy:access-context:bot") - yield* Effect.annotateCurrentSpan("cache.operation", "invalidate") - // Note: We don't have userId here, but invalidation only uses the primary key (botId) - yield* botCache.invalidate(new BotAccessContextRequest({ botId, userId: "" as UserId })) - }), - } satisfies AccessContextCache - }) + return { + getBotContext: Effect.fn("AccessContextCache.getBotContext")(function* ( + botId: BotId, + userId: UserId, + ) { + yield* Effect.annotateCurrentSpan("cache.system", "redis") + yield* Effect.annotateCurrentSpan("cache.name", "electric-proxy:access-context:bot") + yield* Effect.annotateCurrentSpan("cache.operation", "get") + yield* Effect.annotateCurrentSpan("cache.lookup_performed", false) + yield* Effect.annotateCurrentSpan("cache.result", "hit") + const result = yield* botCache.get(new BotAccessContextRequest({ botId, userId })) + return { channelIds: result.channelIds as readonly ChannelId[] } + }), + invalidateBot: Effect.fn("AccessContextCache.invalidateBot")(function* (botId: BotId) { + yield* Effect.annotateCurrentSpan("cache.system", "redis") + yield* Effect.annotateCurrentSpan("cache.name", "electric-proxy:access-context:bot") + yield* Effect.annotateCurrentSpan("cache.operation", "invalidate") + // Note: We don't have userId here, but invalidation only uses the primary key (botId) + yield* botCache.invalidate(new BotAccessContextRequest({ botId, userId: "" as UserId })) + }), + } satisfies AccessContextCache + }), + }, +) { static readonly layer = Layer.effect(this, this.make) } diff --git a/apps/electric-proxy/src/index.ts b/apps/electric-proxy/src/index.ts index 55d35b8cf..decc585f8 100644 --- a/apps/electric-proxy/src/index.ts +++ b/apps/electric-proxy/src/index.ts @@ -1,7 +1,7 @@ import { BunRuntime } from "@effect/platform-bun" import { ProxyAuth } from "@hazel/auth/proxy" import { Database } from "@hazel/db" -import { Effect, Layer, Logger, Metric, Runtime } from "effect" +import { Effect, Layer, Logger, Metric } from "effect" import { validateBotToken } from "./auth/bot-auth" import { validateSession } from "./auth/user-auth" import { @@ -181,7 +181,7 @@ const handleUserRequest = (request: Request) => { }), ), // Fallback for any unhandled errors - returns error details to client for debugging - Effect.catchAll((error) => + Effect.catch((error: unknown) => Effect.gen(function* () { const errorTag = (error as { _tag?: string })?._tag ?? "UnknownError" yield* annotateHandledError(500, errorTag) @@ -376,7 +376,7 @@ const handleBotRequest = (request: Request) => { }), ), // Fallback for any unhandled errors - returns error details to client for debugging - Effect.catch((error) => + Effect.catch((error: unknown) => Effect.gen(function* () { const errorTag = (error as { _tag?: string })?._tag ?? "UnknownError" yield* annotateHandledError(500, errorTag) @@ -399,12 +399,13 @@ const handleBotRequest = (request: Request) => { const duration = Date.now() - start yield* Effect.annotateCurrentSpan("http.status_code", response.status) yield* Effect.annotateCurrentSpan("http.response.status_code", response.status) - yield* Metric.increment(proxyRequestsTotal).pipe( - Effect.tagMetrics({ + yield* Metric.update( + Metric.withAttributes(proxyRequestsTotal, { route: "/bot/v1/shape", auth_type: "bot", status_code: String(response.status), }), + 1, ) yield* Metric.update(proxyRequestDuration, duration) }), @@ -422,7 +423,7 @@ const handleBotRequest = (request: Request) => { // LAYERS // ============================================================================= -const DatabaseLive = Layer.unwrapEffect( +const DatabaseLive = Layer.unwrap( Effect.gen(function* () { const config = yield* ProxyConfigService yield* Effect.log("Connecting to database", { isDev: config.isDev }) @@ -433,10 +434,12 @@ const DatabaseLive = Layer.unwrapEffect( }), ) -const LoggerLive = Layer.unwrapEffect( +const LoggerLive = Layer.unwrap( Effect.gen(function* () { const config = yield* ProxyConfigService - return config.isDev ? Logger.pretty : Logger.structured + return config.isDev + ? Logger.layer([Logger.consolePretty()]) + : Logger.layer([Logger.withConsoleLog(Logger.formatStructured)]) }), ).pipe(Layer.provide(ProxyConfigService.layer)) @@ -467,7 +470,7 @@ const MainLive = DatabaseLive.pipe( // SERVER // ============================================================================= -const ServerLive = Layer.scopedDiscard( +const ServerLive = Layer.effectDiscard( Effect.gen(function* () { const config = yield* ProxyConfigService @@ -483,9 +486,10 @@ const ServerLive = Layer.scopedDiscard( }) } - const runtime = yield* Effect.runtime< + const serviceMap = yield* Effect.services< ProxyConfigService | Database.Database | AccessContextCacheService | ProxyAuth >() + const run = Effect.runPromiseWith(serviceMap) yield* Effect.acquireRelease( Effect.sync(() => @@ -495,8 +499,8 @@ const ServerLive = Layer.scopedDiscard( idleTimeout: 120, routes: { "/health": new Response("OK"), // Static response - zero allocation - "/v1/shape": (req) => Runtime.runPromise(runtime)(handleUserRequest(req)), - "/bot/v1/shape": (req) => Runtime.runPromise(runtime)(handleBotRequest(req)), + "/v1/shape": (req) => run(handleUserRequest(req)), + "/bot/v1/shape": (req) => run(handleBotRequest(req)), }, fetch() { return new Response("Not Found", { status: 404 }) diff --git a/apps/link-preview-worker/src/api.ts b/apps/link-preview-worker/src/api.ts index 6a6e6b537..101a5b9e7 100644 --- a/apps/link-preview-worker/src/api.ts +++ b/apps/link-preview-worker/src/api.ts @@ -5,8 +5,8 @@ export class LinkPreviewApi extends HttpApi.make("api") .add(AppApi) .add(LinkPreviewGroup) .add(TweetGroup) - .annotateContext( - OpenApi.annotate({ + .annotateMerge( + OpenApi.annotations({ title: "Link Preview Worker API", description: "API for fetching link previews and tweet data", }), diff --git a/apps/link-preview-worker/src/declare.ts b/apps/link-preview-worker/src/declare.ts index f1da30e74..708a3379b 100644 --- a/apps/link-preview-worker/src/declare.ts +++ b/apps/link-preview-worker/src/declare.ts @@ -1,12 +1,15 @@ -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" // Health check API export class AppApi extends HttpApiGroup.make("app") - - .add(HttpApiEndpoint.get("health", "/health").addSuccess(Schema.String)) - .annotateContext( - OpenApi.annotate({ + .add( + HttpApiEndpoint.get("health", "/health", { + success: Schema.String, + }), + ) + .annotateMerge( + OpenApi.annotations({ title: "App Api", description: "App Api", }), @@ -22,59 +25,53 @@ export class LinkPreviewData extends Schema.Class("LinkPreviewD publisher: Schema.optional(Schema.String), }) {} -export class LinkPreviewError extends Schema.TaggedErrorClass("LinkPreviewError")( +export class LinkPreviewError extends Schema.TaggedErrorClass()( "LinkPreviewError", { message: Schema.String, }, - HttpApiSchema.status(500), + { httpApiStatus: 500 }, ) {} export class LinkPreviewGroup extends HttpApiGroup.make("linkPreview") .add( - HttpApiEndpoint.get("get")`/` - .addSuccess(LinkPreviewData) - .addError(LinkPreviewError) - .setUrlParams( - Schema.Struct({ - url: Schema.String, - }), - ) - .annotateContext( - OpenApi.annotate({ - title: "Get Link Preview", - description: "Fetch metadata for a given URL", - summary: "Get link preview metadata", - }), - ), + HttpApiEndpoint.get("get", "/", { + payload: { + url: Schema.String, + }, + success: LinkPreviewData, + error: LinkPreviewError, + }).annotateMerge( + OpenApi.annotations({ + title: "Get Link Preview", + description: "Fetch metadata for a given URL", + }), + ), ) .prefix("/link-preview") {} // Tweet Schemas -export class TweetError extends Schema.TaggedErrorClass("TweetError")( +export class TweetError extends Schema.TaggedErrorClass()( "TweetError", { message: Schema.String, }, - HttpApiSchema.status(500), + { httpApiStatus: 500 }, ) {} export class TweetGroup extends HttpApiGroup.make("tweet") .add( - HttpApiEndpoint.get("get")`/` - .addSuccess(Schema.Any) - .addError(TweetError) - .setUrlParams( - Schema.Struct({ - id: Schema.String, - }), - ) - .annotateContext( - OpenApi.annotate({ - title: "Get Tweet", - description: "Fetch tweet data by ID", - summary: "Get tweet metadata", - }), - ), + HttpApiEndpoint.get("get", "/", { + payload: { + id: Schema.String, + }, + success: Schema.Any, + error: TweetError, + }).annotateMerge( + OpenApi.annotations({ + title: "Get Tweet", + description: "Fetch tweet data by ID", + }), + ), ) .prefix("/tweet") {} diff --git a/apps/link-preview-worker/src/handle.ts b/apps/link-preview-worker/src/handle.ts index 7a287ef50..82127ec68 100644 --- a/apps/link-preview-worker/src/handle.ts +++ b/apps/link-preview-worker/src/handle.ts @@ -5,11 +5,7 @@ import { HttpLinkPreviewLive } from "./handlers/link-preview" import { HttpTweetLive } from "./handlers/tweet" export const HttpAppLive = HttpApiBuilder.group(LinkPreviewApi, "app", (handles) => - Effect.gen(function* () { - yield* Effect.logInfo("Link Preview Worker started") - - return handles.handle("health", () => Effect.succeed("ok")) - }), + handles.handle("health", () => Effect.succeed("ok")), ) export { HttpLinkPreviewLive, HttpTweetLive } diff --git a/apps/link-preview-worker/src/handlers/link-preview.ts b/apps/link-preview-worker/src/handlers/link-preview.ts index 522dedfba..66f712c91 100644 --- a/apps/link-preview-worker/src/handlers/link-preview.ts +++ b/apps/link-preview-worker/src/handlers/link-preview.ts @@ -23,14 +23,14 @@ const scraper = metascraper([ ]) // Validate if an image URL is accessible using HttpClient -function validateImageUrl(url: string): Effect.Effect { +function validateImageUrl(url: string) { return HttpClient.head(url).pipe( - Effect.andThen((response) => { + Effect.map((response) => { const contentType = response.headers["content-type"] return contentType ? contentType.startsWith("image/") : false }), Effect.timeout("1 seconds"), - Effect.catch(() => Effect.succeed(false)), + Effect.orElseSucceed(() => false as boolean), Effect.provide(FetchHttpClient.layer), ) } @@ -72,8 +72,8 @@ function extractMetaTag(html: string, property: string): string | null { export const HttpLinkPreviewLive = HttpApiBuilder.group(LinkPreviewApi, "linkPreview", (handlers) => handlers.handle( "get", - Effect.fn(function* ({ urlParams }) { - const targetUrl = urlParams.url + Effect.fn(function* ({ payload }) { + const targetUrl = payload.url const cacheKey = `link-preview:${targetUrl}` const cache = yield* KVCache @@ -87,7 +87,7 @@ export const HttpLinkPreviewLive = HttpApiBuilder.group(LinkPreviewApi, "linkPre logo?: { url: string } publisher?: string }>(cacheKey) - .pipe(Effect.catch(() => Effect.succeed(null))) + .pipe(Effect.orElseSucceed(() => null)) if (cachedData) { yield* Effect.logDebug(`Cache hit for: ${targetUrl}`) @@ -170,11 +170,7 @@ export const HttpLinkPreviewLive = HttpApiBuilder.group(LinkPreviewApi, "linkPre yield* cache .set(cacheKey, result) .pipe( - Effect.catch((error) => - Effect.logDebug(`Failed to cache result: ${error.message}`).pipe( - Effect.andThen(Effect.succeed(undefined)), - ), - ), + Effect.orElseSucceed(() => undefined), ) return result diff --git a/apps/link-preview-worker/src/handlers/tweet.ts b/apps/link-preview-worker/src/handlers/tweet.ts index 82067b164..0259955b1 100644 --- a/apps/link-preview-worker/src/handlers/tweet.ts +++ b/apps/link-preview-worker/src/handlers/tweet.ts @@ -8,14 +8,14 @@ import { TwitterApi, TwitterApiError } from "../services/twitter" export const HttpTweetLive = HttpApiBuilder.group(LinkPreviewApi, "tweet", (handlers) => handlers.handle( "get", - Effect.fn(function* ({ urlParams }) { - const tweetId = urlParams.id + Effect.fn(function* ({ payload }) { + const tweetId = payload.id const cacheKey = `tweet:${tweetId}` const cache = yield* KVCache const twitterApi = yield* TwitterApi // Check cache first - const cachedData = yield* cache.get(cacheKey).pipe(Effect.catch(() => Effect.succeed(null))) + const cachedData = yield* cache.get(cacheKey).pipe(Effect.orElseSucceed(() => null)) if (cachedData) { yield* Effect.logDebug(`Cache hit for tweet: ${tweetId}`) @@ -43,12 +43,7 @@ export const HttpTweetLive = HttpApiBuilder.group(LinkPreviewApi, "tweet", (hand // Store in cache (don't fail request if caching fails) yield* cache.set(cacheKey, tweet).pipe( - Effect.catch((error) => { - const errorMessage = error instanceof Error ? error.message : String(error) - return Effect.logDebug(`Failed to cache tweet: ${errorMessage}`).pipe( - Effect.andThen(Effect.succeed(undefined)), - ) - }), + Effect.orElseSucceed(() => undefined), ) // Return the tweet data diff --git a/apps/link-preview-worker/src/index.ts b/apps/link-preview-worker/src/index.ts index 3ae319ceb..6adacec00 100644 --- a/apps/link-preview-worker/src/index.ts +++ b/apps/link-preview-worker/src/index.ts @@ -1,30 +1,30 @@ import { HttpApiBuilder } from "effect/unstable/httpapi" -import { HttpServer } from "effect/unstable/http" -import { Layer, Logger, pipe } from "effect" +import { HttpRouter, HttpServer } from "effect/unstable/http" +import { Layer, Logger } from "effect" import { LinkPreviewApi } from "./api" import { makeKVCacheLayer } from "./cache" import { HttpAppLive, HttpLinkPreviewLive, HttpTweetLive } from "./handle" import { TwitterApi } from "./services/twitter" -const HttpLive = HttpApiBuilder.api(LinkPreviewApi).pipe( - Layer.provide([HttpAppLive, HttpLinkPreviewLive, HttpTweetLive]), -) +const makeAppLayer = (env: Env) => { + const ServiceLayers = Layer.mergeAll( + makeKVCacheLayer(env.LINK_CACHE), + TwitterApi.layer, + ) + + const HandlerLayers = Layer.mergeAll( + HttpAppLive, + HttpLinkPreviewLive, + HttpTweetLive, + ) -const makeHttpLiveWithKV = (env: Env) => - pipe( - HttpApiBuilder.Router.Live, - Layer.provideMerge(HttpLive), - Layer.provideMerge( - HttpApiBuilder.middlewareCors({ - allowedOrigins: ["http://localhost:3000", "https://app.hazel.sh", "tauri://localhost"], - credentials: true, - }), - ), - Layer.provideMerge(HttpServer.layerContext), - Layer.provide(makeKVCacheLayer(env.LINK_CACHE)), - Layer.provide(TwitterApi.layer), - Layer.provide(Logger.pretty), + return HttpApiBuilder.layer(LinkPreviewApi).pipe( + Layer.provide(HandlerLayers), + HttpRouter.provideRequest(ServiceLayers), + Layer.provide(HttpServer.layerServices), + Layer.provide(Logger.layer([Logger.consolePretty()])), ) +} export default { async fetch(request, env, _ctx): Promise { @@ -32,9 +32,13 @@ export default { env, }) - const Live = makeHttpLiveWithKV(env) - const handler = HttpApiBuilder.toWebHandler(Live, {}) + const Live = makeAppLayer(env) + const { handler, dispose } = HttpRouter.toWebHandler(Live) - return handler.handler(request) + try { + return await handler(request) + } finally { + await dispose() + } }, } satisfies ExportedHandler diff --git a/apps/web/src/atoms/chat-atoms.ts b/apps/web/src/atoms/chat-atoms.ts index 5d1e20da5..804ad80ec 100644 --- a/apps/web/src/atoms/chat-atoms.ts +++ b/apps/web/src/atoms/chat-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import type { AttachmentId, ChannelId, MessageId } from "@hazel/schema" /** diff --git a/apps/web/src/atoms/chat-query-atoms.ts b/apps/web/src/atoms/chat-query-atoms.ts index 6b00d7880..97a220191 100644 --- a/apps/web/src/atoms/chat-query-atoms.ts +++ b/apps/web/src/atoms/chat-query-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import type { Message, PinnedMessage, User } from "@hazel/domain/models" import type { ChannelId } from "@hazel/schema" import { eq } from "@tanstack/db" diff --git a/apps/web/src/atoms/command-palette-state.ts b/apps/web/src/atoms/command-palette-state.ts index 1e82000ef..48f51b736 100644 --- a/apps/web/src/atoms/command-palette-state.ts +++ b/apps/web/src/atoms/command-palette-state.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import type { FilterType, SearchFilter } from "~/lib/search-filter-parser" /** diff --git a/apps/web/src/atoms/custom-emoji-atoms.ts b/apps/web/src/atoms/custom-emoji-atoms.ts index 18e3dd0e4..5c64473a5 100644 --- a/apps/web/src/atoms/custom-emoji-atoms.ts +++ b/apps/web/src/atoms/custom-emoji-atoms.ts @@ -1,4 +1,4 @@ -import { Atom, Result } from "@effect/atom-react" +import { Atom, AsyncResult } from "effect/unstable/reactivity" import type { OrganizationId } from "@hazel/schema" import { and, eq, isNull } from "@tanstack/db" import { customEmojiCollection } from "~/db/collections" @@ -24,7 +24,7 @@ export const customEmojisForOrgAtomFamily = Atom.family((orgId: OrganizationId) export const customEmojiMapAtomFamily = Atom.family((orgId: OrganizationId) => Atom.make((get) => { const emojisResult = get(customEmojisForOrgAtomFamily(orgId)) - const emojis = Result.getOrElse(emojisResult, () => []) + const emojis = AsyncResult.getOrElse(emojisResult, () => []) const map = new Map() for (const emoji of emojis) { diff --git a/apps/web/src/atoms/desktop-auth.ts b/apps/web/src/atoms/desktop-auth.ts index fe513d470..3ac1fe904 100644 --- a/apps/web/src/atoms/desktop-auth.ts +++ b/apps/web/src/atoms/desktop-auth.ts @@ -7,7 +7,7 @@ * This module owns atom definitions, init, login, logout, and the scheduler. */ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import { Clipboard } from "@effect/platform-browser" import type { OrganizationId } from "@hazel/schema" import { Duration, Effect, Layer, Option, Schema } from "effect" @@ -96,9 +96,10 @@ export const desktopLoginAtom = Atom.fn( const auth = yield* TauriAuth const authResult = yield* auth.initiateAuth(options) - const accessTokenOpt = yield* TokenStorage.getAccessToken - const refreshTokenOpt = yield* TokenStorage.getRefreshToken - const expiresAtOpt = yield* TokenStorage.getExpiresAt + const tokenStorage = yield* TokenStorage + const accessTokenOpt = yield* tokenStorage.getAccessToken + const refreshTokenOpt = yield* tokenStorage.getRefreshToken + const expiresAtOpt = yield* tokenStorage.getExpiresAt if ( Option.isSome(accessTokenOpt) && @@ -142,7 +143,10 @@ export const desktopLogoutAtom = Atom.fn( return } - yield* TokenStorage.clearTokens.pipe( + yield* Effect.gen(function* () { + const tokenStorage = yield* TokenStorage + yield* tokenStorage.clearTokens + }).pipe( Effect.provide(TokenStorageLive), Effect.catch((error) => { console.error("[desktop-auth] Failed to clear tokens:", error) @@ -197,7 +201,7 @@ export const desktopLoginFromClipboardAtom = Atom.fn( catch: () => new Error("Invalid clipboard data - not valid JSON"), }) - const parsed = yield* Schema.decodeUnknown(ClipboardAuthPayload)(rawJson).pipe( + const parsed = yield* Schema.decodeUnknownEffect(ClipboardAuthPayload)(rawJson).pipe( Effect.mapError(() => new Error("Invalid clipboard data - missing code or state")), ) @@ -206,7 +210,8 @@ export const desktopLoginFromClipboardAtom = Atom.fn( const tokenExchange = yield* TokenExchange const tokens = yield* tokenExchange.exchangeCode(parsed.code, stateString) - yield* TokenStorage.storeTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn) + const tokenStorage = yield* TokenStorage + yield* tokenStorage.storeTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn) return tokens }).pipe( @@ -242,9 +247,10 @@ export const desktopInitAtom = Atom.make((get) => { if (!isTauri()) return null const loadTokens = Effect.gen(function* () { - const accessTokenOpt = yield* TokenStorage.getAccessToken - const refreshTokenOpt = yield* TokenStorage.getRefreshToken - const expiresAtOpt = yield* TokenStorage.getExpiresAt + const tokenStorage = yield* TokenStorage + const accessTokenOpt = yield* tokenStorage.getAccessToken + const refreshTokenOpt = yield* tokenStorage.getRefreshToken + const expiresAtOpt = yield* tokenStorage.getExpiresAt if (Option.isSome(accessTokenOpt) && Option.isSome(refreshTokenOpt) && Option.isSome(expiresAtOpt)) { get.set(desktopTokensAtom, { @@ -274,7 +280,7 @@ export const desktopInitAtom = Atom.make((get) => { const fiber = runtime.runFork(loadTokens) get.addFinalizer(() => { - fiber.unsafeInterruptAsFork(fiber.id()) + fiber.interruptUnsafe() }) return null @@ -314,7 +320,7 @@ export const desktopTokenSchedulerAtom = Atom.make((get) => { const fiber = runtime.runFork(refreshSchedule) get.addFinalizer(() => { - fiber.unsafeInterruptAsFork(fiber.id()) + fiber.interruptUnsafe() }) return { scheduledFor, immediate: false } @@ -333,7 +339,10 @@ export const clearDesktopTokens = (): Promise => { if (!isTauri()) return Promise.resolve() return runtime.runPromise( - TokenStorage.clearTokens.pipe( + Effect.gen(function* () { + const tokenStorage = yield* TokenStorage + yield* tokenStorage.clearTokens + }).pipe( Effect.provide(TokenStorageLive), Effect.catch((error) => { console.error("[desktop-auth] Failed to clear tokens during recovery:", error) diff --git a/apps/web/src/atoms/desktop-callback-atoms.ts b/apps/web/src/atoms/desktop-callback-atoms.ts index f47b118c4..eae50056b 100644 --- a/apps/web/src/atoms/desktop-callback-atoms.ts +++ b/apps/web/src/atoms/desktop-callback-atoms.ts @@ -4,7 +4,7 @@ * @description Effect Atom-based state management for desktop OAuth callback handling */ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import { DesktopConnectionError, InvalidDesktopStateError, @@ -249,7 +249,7 @@ export const createCallbackInitAtom = (params: DesktopCallbackParams) => const fiber = runtime.runFork(callbackEffect) get.addFinalizer(() => { - fiber.unsafeInterruptAsFork(fiber.id()) + fiber.interruptUnsafe() }) return null diff --git a/apps/web/src/atoms/emoji-atoms.ts b/apps/web/src/atoms/emoji-atoms.ts index c44cfa153..f75487efc 100644 --- a/apps/web/src/atoms/emoji-atoms.ts +++ b/apps/web/src/atoms/emoji-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/feature-discovery-atoms.ts b/apps/web/src/atoms/feature-discovery-atoms.ts index 75f9403c3..9953d07e6 100644 --- a/apps/web/src/atoms/feature-discovery-atoms.ts +++ b/apps/web/src/atoms/feature-discovery-atoms.ts @@ -1,4 +1,5 @@ -import { Atom, useAtomSet, useAtomValue } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { Schema } from "effect" import { useCallback } from "react" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/hotkey-atoms.ts b/apps/web/src/atoms/hotkey-atoms.ts index c90a4e9de..4d11c7c08 100644 --- a/apps/web/src/atoms/hotkey-atoms.ts +++ b/apps/web/src/atoms/hotkey-atoms.ts @@ -1,4 +1,5 @@ -import { Atom, useAtomSet, useAtomValue } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { normalizeHotkey, validateHotkey, type Hotkey } from "@tanstack/react-hotkeys" import { Schema } from "effect" import { useCallback } from "react" diff --git a/apps/web/src/atoms/loading-state-atoms.ts b/apps/web/src/atoms/loading-state-atoms.ts index d57d3b023..eb53115b5 100644 --- a/apps/web/src/atoms/loading-state-atoms.ts +++ b/apps/web/src/atoms/loading-state-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" /** * Loading state enum diff --git a/apps/web/src/atoms/message-atoms.ts b/apps/web/src/atoms/message-atoms.ts index 1718aa463..b7bb2cc01 100644 --- a/apps/web/src/atoms/message-atoms.ts +++ b/apps/web/src/atoms/message-atoms.ts @@ -1,4 +1,4 @@ -import { Atom, Result } from "@effect/atom-react" +import { Atom, AsyncResult } from "effect/unstable/reactivity" import type { ChannelId, MessageId, UserId } from "@hazel/schema" import { and, count, eq, isNull } from "@tanstack/db" import { @@ -35,7 +35,7 @@ export const processedReactionsAtomFamily = Atom.family((key: `${MessageId}:${st const [messageId] = key.split(":") as [MessageId, string] const currentUserId = key.slice(messageId.length + 1) // Handle userId that may contain ":" const reactionsResult = get(messageReactionsAtomFamily(messageId)) - const reactions = Result.getOrElse(reactionsResult, () => []) + const reactions = AsyncResult.getOrElse(reactionsResult, () => []) // Aggregate reactions by emoji return Object.entries( diff --git a/apps/web/src/atoms/modal-atoms.ts b/apps/web/src/atoms/modal-atoms.ts index 5bca7b0df..b88edcb20 100644 --- a/apps/web/src/atoms/modal-atoms.ts +++ b/apps/web/src/atoms/modal-atoms.ts @@ -1,4 +1,5 @@ -import { Atom, useAtomSet, useAtomValue } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { useCallback } from "react" /** diff --git a/apps/web/src/atoms/notification-sound-atoms.ts b/apps/web/src/atoms/notification-sound-atoms.ts index 6dc89ed59..28ccb3c20 100644 --- a/apps/web/src/atoms/notification-sound-atoms.ts +++ b/apps/web/src/atoms/notification-sound-atoms.ts @@ -3,7 +3,7 @@ * @description Atoms for notification sound system state management */ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/onboarding-atoms.ts b/apps/web/src/atoms/onboarding-atoms.ts index 25e9767ea..52646743e 100644 --- a/apps/web/src/atoms/onboarding-atoms.ts +++ b/apps/web/src/atoms/onboarding-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import type { OrganizationId } from "@hazel/schema" // Step identifiers diff --git a/apps/web/src/atoms/organization-atoms.ts b/apps/web/src/atoms/organization-atoms.ts index 3e1397baf..198e063d2 100644 --- a/apps/web/src/atoms/organization-atoms.ts +++ b/apps/web/src/atoms/organization-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import type { OrganizationId } from "@hazel/schema" import { HazelRpcClient } from "~/lib/services/common/rpc-atom-client" diff --git a/apps/web/src/atoms/panel-atoms.ts b/apps/web/src/atoms/panel-atoms.ts index 33f280a8f..09a5956bd 100644 --- a/apps/web/src/atoms/panel-atoms.ts +++ b/apps/web/src/atoms/panel-atoms.ts @@ -1,4 +1,5 @@ -import { Atom, useAtomSet, useAtomValue } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { Schema } from "effect" import { useCallback } from "react" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/presence-atoms.ts b/apps/web/src/atoms/presence-atoms.ts index 741044a3a..ca06b224e 100644 --- a/apps/web/src/atoms/presence-atoms.ts +++ b/apps/web/src/atoms/presence-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" /** * Shared "now" signal used to periodically re-render presence UI. diff --git a/apps/web/src/atoms/react-scan-atoms.ts b/apps/web/src/atoms/react-scan-atoms.ts index 08ba83d04..b7386cdb3 100644 --- a/apps/web/src/atoms/react-scan-atoms.ts +++ b/apps/web/src/atoms/react-scan-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/recent-channels-atom.ts b/apps/web/src/atoms/recent-channels-atom.ts index 8b48a32ac..d5b22a03f 100644 --- a/apps/web/src/atoms/recent-channels-atom.ts +++ b/apps/web/src/atoms/recent-channels-atom.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/search-atoms.ts b/apps/web/src/atoms/search-atoms.ts index 6d7580dd5..6845c01eb 100644 --- a/apps/web/src/atoms/search-atoms.ts +++ b/apps/web/src/atoms/search-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/section-collapse-atoms.ts b/apps/web/src/atoms/section-collapse-atoms.ts index 58d30f0ce..186561e60 100644 --- a/apps/web/src/atoms/section-collapse-atoms.ts +++ b/apps/web/src/atoms/section-collapse-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import type { ChannelSectionId } from "@hazel/schema" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/sidebar-atoms.ts b/apps/web/src/atoms/sidebar-atoms.ts index e8b16e7d1..a086f4fc8 100644 --- a/apps/web/src/atoms/sidebar-atoms.ts +++ b/apps/web/src/atoms/sidebar-atoms.ts @@ -1,4 +1,4 @@ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import { Schema } from "effect" import { platformStorageRuntime } from "~/lib/platform-storage" diff --git a/apps/web/src/atoms/tauri-update-atoms.ts b/apps/web/src/atoms/tauri-update-atoms.ts index 882f4aa8c..f2a054427 100644 --- a/apps/web/src/atoms/tauri-update-atoms.ts +++ b/apps/web/src/atoms/tauri-update-atoms.ts @@ -4,7 +4,7 @@ * @description Effect Atom-based state management for Tauri app updates */ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import { getTauriProcess, getTauriUpdater, diff --git a/apps/web/src/atoms/web-auth.ts b/apps/web/src/atoms/web-auth.ts index d3f989ddf..e1a2f8e39 100644 --- a/apps/web/src/atoms/web-auth.ts +++ b/apps/web/src/atoms/web-auth.ts @@ -7,7 +7,7 @@ * This module owns atom definitions, init, logout, and the scheduler. */ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import { Duration, Effect, Option, Schema } from "effect" import { runtime } from "~/lib/services/common/runtime" import { WebTokenStorage } from "~/lib/services/web/token-storage" @@ -213,7 +213,7 @@ export const webInitAtom = Atom.make((get) => { const fiber = runtime.runFork(loadTokens) get.addFinalizer(() => { - fiber.unsafeInterruptAsFork(fiber.id()) + fiber.interruptUnsafe() }) return null @@ -253,7 +253,7 @@ export const webTokenSchedulerAtom = Atom.make((get) => { const fiber = runtime.runFork(refreshSchedule) get.addFinalizer(() => { - fiber.unsafeInterruptAsFork(fiber.id()) + fiber.interruptUnsafe() }) return { scheduledFor, immediate: false } diff --git a/apps/web/src/atoms/web-callback-atoms.ts b/apps/web/src/atoms/web-callback-atoms.ts index 90bfc6e70..248a69150 100644 --- a/apps/web/src/atoms/web-callback-atoms.ts +++ b/apps/web/src/atoms/web-callback-atoms.ts @@ -4,7 +4,7 @@ * @description Effect Atom-based state management for web OAuth callback handling (JWT flow) */ -import { Atom } from "@effect/atom-react" +import { Atom } from "effect/unstable/reactivity" import { MissingAuthCodeError, OAuthCallbackError, diff --git a/apps/web/src/components/channel-settings/add-github-repo-modal.tsx b/apps/web/src/components/channel-settings/add-github-repo-modal.tsx index d4c6b310b..952e607ed 100644 --- a/apps/web/src/components/channel-settings/add-github-repo-modal.tsx +++ b/apps/web/src/components/channel-settings/add-github-repo-modal.tsx @@ -1,4 +1,5 @@ -import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" +import { AsyncResult } from "effect/unstable/reactivity" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import type { GitHubSubscription } from "@hazel/domain/models" import type { ChannelId } from "@hazel/schema" diff --git a/apps/web/src/components/chat-sync/add-channel-link-modal.tsx b/apps/web/src/components/chat-sync/add-channel-link-modal.tsx index 367d6bf33..08ae576df 100644 --- a/apps/web/src/components/chat-sync/add-channel-link-modal.tsx +++ b/apps/web/src/components/chat-sync/add-channel-link-modal.tsx @@ -1,4 +1,5 @@ -import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" +import { AsyncResult } from "effect/unstable/reactivity" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { Channel } from "@hazel/domain/models" import type { ChannelId, ExternalChannelId, OrganizationId, SyncConnectionId } from "@hazel/schema" import { eq, or, useLiveQuery } from "@tanstack/react-db" @@ -123,7 +124,7 @@ export function AddChannelLinkModal({ const discordChannels = useMemo( () => - Result.builder(discordChannelsResult) + AsyncResult.builder(discordChannelsResult) .onSuccess((data) => data?.channels ?? []) .orElse(() => []), [discordChannelsResult], @@ -251,7 +252,7 @@ export function AddChannelLinkModal({
- {Result.isInitial(discordChannelsResult) && ( + {AsyncResult.isInitial(discordChannelsResult) && (
@@ -259,7 +260,7 @@ export function AddChannelLinkModal({
)} - {Result.isFailure(discordChannelsResult) && ( + {AsyncResult.isFailure(discordChannelsResult) && (

Could not load Discord channels

@@ -267,7 +268,7 @@ export function AddChannelLinkModal({

)} - {Result.isSuccess(discordChannelsResult) && ( + {AsyncResult.isSuccess(discordChannelsResult) && ( <> {selectedDiscordChannel ? (
@@ -367,7 +368,7 @@ export function AddChannelLinkModal({
)} - {Result.isSuccess(guildsResult) && ( + {AsyncResult.isSuccess(guildsResult) && (
{selectedGuild ? ( @@ -222,7 +223,7 @@ export function AddConnectionModal({
- {Result.isInitial(channelLinksResult) ? ( + {AsyncResult.isInitial(channelLinksResult) ? (
diff --git a/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/index.tsx b/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/index.tsx index 4f67cdd83..cedf140b2 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/index.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/index.tsx @@ -1,4 +1,5 @@ -import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" +import { AsyncResult } from "effect/unstable/reactivity" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { SyncConnectionId } from "@hazel/schema" import { createFileRoute, useNavigate } from "@tanstack/react-router" import { Option } from "effect" @@ -141,7 +142,7 @@ function ChatSyncConnectionsPage() { } // Loading state - if (Result.isInitial(connectionsResult)) { + if (AsyncResult.isInitial(connectionsResult)) { return ( <> @@ -165,7 +166,7 @@ function ChatSyncConnectionsPage() { } // Error state - if (Result.isFailure(connectionsResult)) { + if (AsyncResult.isFailure(connectionsResult)) { return ( <> @@ -186,7 +187,7 @@ function ChatSyncConnectionsPage() { ) } - const data = Result.value(connectionsResult) + const data = AsyncResult.value(connectionsResult) const connections = Option.isSome(data) ? data.value.data : [] return ( diff --git a/apps/web/src/routes/_app/$orgSlug/settings/connect-invites.tsx b/apps/web/src/routes/_app/$orgSlug/settings/connect-invites.tsx index 90cf5166f..d916017bb 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/connect-invites.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/connect-invites.tsx @@ -1,4 +1,5 @@ -import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" +import { AsyncResult } from "effect/unstable/reactivity" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { ConnectInviteId, OrganizationId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { Option } from "effect" @@ -29,8 +30,8 @@ function ConnectInvitesPage() { const invitesResult = useAtomValue(listIncomingInvitesQuery(organizationId!)) const allInvites = useMemo(() => { - if (!Result.isSuccess(invitesResult)) return [] - const data = Result.value(invitesResult) + if (!AsyncResult.isSuccess(invitesResult)) return [] + const data = AsyncResult.value(invitesResult) if (Option.isNone(data)) return [] return data.value.data }, [invitesResult]) diff --git a/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx b/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx index 319288d58..0e0037b60 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx @@ -1,4 +1,5 @@ -import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" +import { AsyncResult } from "effect/unstable/reactivity" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { OrganizationId } from "@hazel/schema" import type { IntegrationConnection } from "@hazel/domain/models" import { createFileRoute, notFound, useNavigate } from "@tanstack/react-router" @@ -768,7 +769,7 @@ function GitHubRepositoryAccessSection({ organizationId }: { organizationId: Org ) // Loading state - if (Result.isInitial(repositoriesResult)) { + if (AsyncResult.isInitial(repositoriesResult)) { return (
@@ -799,7 +800,7 @@ function GitHubRepositoryAccessSection({ organizationId }: { organizationId: Org } // Error state - if (Result.isFailure(repositoriesResult)) { + if (AsyncResult.isFailure(repositoriesResult)) { return (
@@ -812,7 +813,7 @@ function GitHubRepositoryAccessSection({ organizationId }: { organizationId: Org ) } - const data = Result.value(repositoriesResult) + const data = AsyncResult.value(repositoriesResult) if (Option.isNone(data)) { return null } diff --git a/apps/web/src/routes/join/$slug.tsx b/apps/web/src/routes/join/$slug.tsx index 7a36624c9..f85b38e25 100644 --- a/apps/web/src/routes/join/$slug.tsx +++ b/apps/web/src/routes/join/$slug.tsx @@ -1,4 +1,5 @@ -import { Result, useAtomSet, useAtomValue } from "@effect/atom-react" +import { AsyncResult } from "effect/unstable/reactivity" +import { useAtomSet, useAtomValue } from "@effect/atom-react" import { createFileRoute, Link, useNavigate } from "@tanstack/react-router" import { Match } from "effect" import { motion } from "motion/react" @@ -67,7 +68,7 @@ function JoinPage() { const orgResult = useAtomValue(getOrgBySlugPublicQuery(slug)) const joinOrg = useAtomSet(joinViaPublicInviteMutation, { mode: "promiseExit" }) - const isLoading = orgResult._tag === "Initial" || orgResult.waiting + const isLoading = orgAsyncResult._tag === "Initial" || orgAsyncResult.waiting const handleSignIn = () => { login({ @@ -158,7 +159,7 @@ function JoinPage() { ) } - const org = Result.getOrElse(orgResult, () => null) + const org = AsyncResult.getOrElse(orgResult, () => null) // Organization not found or not public if (!org) { diff --git a/bots/hazel-bot/src/agent-loop.ts b/bots/hazel-bot/src/agent-loop.ts index fabeefd63..8813325dc 100644 --- a/bots/hazel-bot/src/agent-loop.ts +++ b/bots/hazel-bot/src/agent-loop.ts @@ -1,13 +1,15 @@ import { AiError, LanguageModel, Prompt, type Response, type Toolkit } from "effect/unstable/ai" -import { Duration, Effect, Mailbox, Stream } from "effect" +import { Cause, Duration, Effect, Queue, Stream } from "effect" import { withDegenerationDetection } from "./degeneration-detector.ts" -import { IterationTimeoutError, StreamIdleTimeoutError } from "./errors.ts" +import { DegenerateOutputError, IterationTimeoutError, StreamIdleTimeoutError } from "./errors.ts" const MAX_ITERATIONS = 10 const IDLE_TIMEOUT = Duration.seconds(15) const ITERATION_TIMEOUT = Duration.minutes(2) +type AgentError = AiError.AiError | StreamIdleTimeoutError | IterationTimeoutError | DegenerateOutputError + /** * Multi-step streaming agent loop. * @@ -17,10 +19,10 @@ const ITERATION_TIMEOUT = Duration.minutes(2) * again, until it responds without tool calls or MAX_ITERATIONS is reached. * * All stream parts (text deltas, tool calls, tool results) from every iteration - * are emitted in real-time via a Mailbox-backed stream. + * are emitted in real-time via a Queue-backed stream. * * Safeguards per iteration: - * - Idle timeout: fails if no chunk received for 15s + * - Idle timeout: fails if no chunk received for 15s (via stream timeout + concat fail) * - Degeneration detection: fails if repetitive patterns detected * - Iteration timeout: fails if a single LLM call exceeds 2 minutes */ @@ -29,14 +31,12 @@ export const streamAgentLoop = (options: { toolkit: Toolkit.WithHandler }): Stream.Stream< Response.AnyPart, - AiError.AiError | StreamIdleTimeoutError | IterationTimeoutError, + AgentError, LanguageModel.LanguageModel > => Effect.gen(function* () { - const mailbox = yield* Mailbox.make< - Response.AnyPart, - AiError.AiError | StreamIdleTimeoutError | IterationTimeoutError - >() + const queue: Queue.Queue = + yield* Queue.make() yield* Effect.gen(function* () { let currentPrompt = Prompt.make(options.prompt) @@ -49,19 +49,21 @@ export const streamAgentLoop = (options: { toolkit: options.toolkit, toolChoice: "auto" as any, }).pipe( - // Idle timeout: resets on each emitted element - Stream.timeoutFail( - () => + // Idle timeout: ends the stream if no element arrives for 15s, + // then we concat a failure stream so that timeout = error + Stream.timeout(IDLE_TIMEOUT), + Stream.concat( + Stream.fail( new StreamIdleTimeoutError({ message: "No data received from AI model for 15 seconds", }), - IDLE_TIMEOUT, + ), ), // Degeneration detection: fails on repetitive patterns withDegenerationDetection, Stream.runForEach((part) => { collectedParts.push(part as Response.AnyPart) - return mailbox.offer(part as Response.AnyPart) + return Queue.offer(queue, part as Response.AnyPart) }), // Iteration timeout: wall-clock limit per LLM call Effect.timeoutOrElse({ @@ -80,13 +82,13 @@ export const streamAgentLoop = (options: { if (!hasToolCalls) break // Append assistant response + tool results to prompt for next iteration - currentPrompt = Prompt.merge(currentPrompt, Prompt.fromResponseParts(collectedParts)) + currentPrompt = Prompt.concat(currentPrompt, Prompt.fromResponseParts(collectedParts)) } - }).pipe(Mailbox.into(mailbox), Effect.forkScoped) + }).pipe(Queue.into(queue), Effect.forkScoped) - return Mailbox.toStream(mailbox) - }).pipe(Stream.unwrapScoped) as Stream.Stream< + return Stream.fromQueue(queue) + }).pipe(Stream.unwrap) as Stream.Stream< Response.AnyPart, - AiError.AiError | StreamIdleTimeoutError | IterationTimeoutError, + AgentError, LanguageModel.LanguageModel > diff --git a/bots/hazel-bot/src/degeneration-detector.ts b/bots/hazel-bot/src/degeneration-detector.ts index 53a768959..cb44d7e31 100644 --- a/bots/hazel-bot/src/degeneration-detector.ts +++ b/bots/hazel-bot/src/degeneration-detector.ts @@ -20,26 +20,38 @@ const MIN_REPEATS = 8 export const withDegenerationDetection = ( stream: Stream.Stream, ): Stream.Stream => - Stream.mapAccumEffect(stream, "", (window, part: Response.AnyPart) => { - if (part.type !== "text-delta") { - return Effect.succeed([window, part] as const) - } + stream.pipe( + Stream.mapAccum( + () => "", + (window: string, part: Response.AnyPart) => { + if (part.type !== "text-delta") { + return [window, [part]] as const + } - const updated = (window + part.delta).slice(-WINDOW_SIZE) - const detected = findRepetition(updated) + const updated = (window + part.delta).slice(-WINDOW_SIZE) + const detected = findRepetition(updated) - if (detected) { - return Effect.fail( - new DegenerateOutputError({ - message: `Detected repeating pattern "${detected.pattern}" (${detected.repeats}x)`, - pattern: detected.pattern, - repeats: detected.repeats, - }), - ) - } + if (detected) { + // Return a sentinel value to trigger failure after mapAccum + return [updated, [{ __degenerate: true, pattern: detected.pattern, repeats: detected.repeats } as any]] as const + } - return Effect.succeed([updated, part] as const) - }) + return [updated, [part]] as const + }, + ), + Stream.filterEffect((part: any) => { + if (part.__degenerate) { + return Effect.fail( + new DegenerateOutputError({ + message: `Detected repeating pattern "${part.pattern}" (${part.repeats}x)`, + pattern: part.pattern, + repeats: part.repeats, + }), + ) + } + return Effect.succeed(true) + }), + ) as Stream.Stream /** * Scans the tail of the window for any substring of length 2-10 that repeats diff --git a/bots/hazel-bot/src/handler.ts b/bots/hazel-bot/src/handler.ts index 18cc25410..fa318cfe3 100644 --- a/bots/hazel-bot/src/handler.ts +++ b/bots/hazel-bot/src/handler.ts @@ -44,7 +44,7 @@ export const handleAIRequest = (params: { Effect.gen(function* () { const { bot, message, channelId, orgId } = params - const enabledIntegrations = yield* bot.integration.getEnabled(orgId) + const enabledIntegrations = yield* (bot as any).integration.getEnabled(orgId) yield* Effect.log(`Enabled integrations for org ${orgId}:`, { integrations: Array.from(enabledIntegrations), @@ -78,7 +78,7 @@ export const handleAIRequest = (params: { // Use acquireUseRelease for guaranteed cleanup of the streaming session. yield* Effect.acquireUseRelease( - bot.ai.stream(channelId, { + (bot as any).ai.stream(channelId, { model: modelName, showThinking: true, showToolCalls: true, @@ -88,7 +88,7 @@ export const handleAIRequest = (params: { throbbing: true, }, }), - (session) => + (session: any) => Effect.gen(function* () { yield* Effect.log(`Created streaming message ${session.messageId}`) @@ -124,21 +124,30 @@ export const handleAIRequest = (params: { }), ), // Release: on failure/interrupt, persist the error state - (session, exit) => + (session: any, exit) => Exit.isSuccess(exit) ? Effect.void : Effect.gen(function* () { const cause = exit.cause yield* Effect.logError("Agent streaming failed", { error: cause }) - const userMessage: string = Cause.match(cause, { - onEmpty: "Request was cancelled.", - onFail: (error) => mapErrorToUserMessage(error), - onDie: () => "An unexpected error occurred.", - onInterrupt: () => "Request was cancelled.", - onSequential: (left: string) => left, - onParallel: (left: string) => left, - }) + // Extract a user-facing message from the cause + const failReason = cause.reasons.find(Cause.isFailReason) + const dieReason = cause.reasons.find(Cause.isDieReason) + const interruptReason = cause.reasons.find(Cause.isInterruptReason) + + let userMessage: string + if (failReason) { + userMessage = mapErrorToUserMessage(failReason.error) + } else if (dieReason) { + userMessage = "An unexpected error occurred." + } else if (interruptReason) { + userMessage = "Request was cancelled." + } else if (cause.reasons.length === 0) { + userMessage = "Request was cancelled." + } else { + userMessage = "An unexpected error occurred." + } yield* session.fail(userMessage).pipe(Effect.ignore) }), @@ -147,9 +156,9 @@ export const handleAIRequest = (params: { // Provide the LanguageModel dynamically based on config Effect.provideServiceEffect( LanguageModel.LanguageModel, - Config.string("AI_MODEL").pipe( - Config.withDefault("google/gemini-3-flash-preview"), - Effect.flatMap((model) => makeOpenRouterModel(model)), - ), + Effect.gen(function* () { + const model = yield* Config.string("AI_MODEL").pipe(Config.withDefault("google/gemini-3-flash-preview")) + return yield* makeOpenRouterModel(model) + }), ), ) diff --git a/bots/hazel-bot/src/index.ts b/bots/hazel-bot/src/index.ts index 06b937cfb..29f7fb8a6 100644 --- a/bots/hazel-bot/src/index.ts +++ b/bots/hazel-bot/src/index.ts @@ -12,7 +12,7 @@ const ACTIVE_THREADS_KEY = "hazel-bot:active-threads" const ActiveThreadsState = Schema.Struct({ order: Schema.Array(Schema.String), - byChannel: Schema.Record({ key: Schema.String, value: Schema.String }), + byChannel: Schema.Record(Schema.String, Schema.String), }) const defaultActiveThreadsState = () => ({ @@ -27,18 +27,20 @@ const bot = defineBot({ layers: [LinearApiClient.layer, CraftApiClient.layer], setup: (bot) => Effect.gen(function* () { + const botAny = bot as any + const loadActiveThreads = () => - bot.state + botAny.state .getJson(ACTIVE_THREADS_KEY, ActiveThreadsState) - .pipe(Effect.map((state) => state ?? defaultActiveThreadsState())) + .pipe(Effect.map((state: any) => state ?? defaultActiveThreadsState())) const saveActiveThreads = (state: Schema.Schema.Type) => - bot.state.setJson(ACTIVE_THREADS_KEY, ActiveThreadsState, state) + botAny.state.setJson(ACTIVE_THREADS_KEY, ActiveThreadsState, state) const trackThread = (channelId: string, orgId: OrganizationId) => Effect.gen(function* () { const current = yield* loadActiveThreads() - const nextOrder = current.order.filter((id) => id !== channelId) + const nextOrder = current.order.filter((id: string) => id !== channelId) const nextByChannel = { ...current.byChannel } nextOrder.push(channelId) @@ -61,7 +63,7 @@ const bot = defineBot({ }) // /ask command handler - yield* bot.onCommand(AskCommand, (ctx) => + yield* botAny.onCommand(AskCommand, (ctx: any) => Effect.gen(function* () { yield* Effect.log(`Received /ask: ${ctx.args.message}`) yield* handleAIRequest({ @@ -74,11 +76,11 @@ const bot = defineBot({ ) // /test command handler - yield* bot.onCommand(TestCommand, (ctx) => + yield* botAny.onCommand(TestCommand, (ctx: any) => Effect.gen(function* () { const now = new Date(ctx.timestamp).toISOString() yield* Effect.log(`Received /test in ${ctx.channelId}`) - yield* bot.message.send( + yield* botAny.message.send( ctx.channelId, `Hazel Bot gateway test OK.\nchannel=${ctx.channelId}\norg=${ctx.orgId}\ntimestamp=${now}`, ) @@ -86,9 +88,9 @@ const bot = defineBot({ ) // Thread follow-up handler - yield* bot.onMessage((message) => + yield* botAny.onMessage((message: any) => Effect.gen(function* () { - const authContext = yield* bot.getAuthContext + const authContext = yield* botAny.getAuthContext // Skip bot's own messages to prevent infinite loops if (message.authorId === authContext.userId) return @@ -101,12 +103,12 @@ const bot = defineBot({ yield* Effect.log(`Thread follow-up in ${message.channelId}: ${message.content}`) // Fetch thread history for conversation context - const { data: messages } = yield* bot.message.list(message.channelId, { + const { data: messages } = yield* botAny.message.list(message.channelId, { limit: 50, }) // messages are newest-first, reverse to chronological order - const history = [...messages].reverse().map((m) => ({ + const history = [...messages].reverse().map((m: any) => ({ role: (m.authorId === authContext.userId ? "assistant" : "user") as | "user" | "assistant", @@ -124,10 +126,10 @@ const bot = defineBot({ ) // @mention handler — reply in a thread - yield* bot.onMention((message) => + yield* botAny.onMention((message: any) => Effect.gen(function* () { yield* Effect.log(`Received @mention: ${message.content}`) - const authContext = yield* bot.getAuthContext + const authContext = yield* botAny.getAuthContext // Strip the bot mention from content to get the question const question = message.content @@ -137,12 +139,12 @@ const bot = defineBot({ yield* Effect.log(`Received question: ${question}`) if (!question) { - yield* bot.message.reply(message, "Hey! What can I help you with?") + yield* botAny.message.reply(message, "Hey! What can I help you with?") return } // Resolve thread + org context - const thread = yield* bot.channel.createThread(message.id, message.channelId) + const thread = yield* botAny.channel.createThread(message.id, message.channelId) yield* Effect.log(`Created thread: ${thread.id}`) diff --git a/bots/hazel-bot/src/tools/base.ts b/bots/hazel-bot/src/tools/base.ts index ab08bb129..6bff1cc74 100644 --- a/bots/hazel-bot/src/tools/base.ts +++ b/bots/hazel-bot/src/tools/base.ts @@ -8,12 +8,12 @@ export const GetCurrentTime = Tool.make("get_current_time", { export const Calculate = Tool.make("calculate", { description: "Perform basic arithmetic calculations", - parameters: { + parameters: Schema.Struct({ operation: Schema.Literals(["add", "subtract", "multiply", "divide"]).annotate({ description: "The arithmetic operation to perform", }), a: Schema.Number.annotate({ description: "First operand" }), b: Schema.Number.annotate({ description: "Second operand" }), - }, + }), success: Schema.Number, }) diff --git a/bots/hazel-bot/src/tools/craft.ts b/bots/hazel-bot/src/tools/craft.ts index 16a7fdb79..5a21ff2ec 100644 --- a/bots/hazel-bot/src/tools/craft.ts +++ b/bots/hazel-bot/src/tools/craft.ts @@ -3,23 +3,23 @@ import { Schema } from "effect" export const CraftSearchDocuments = Tool.make("craft_search_documents", { description: "Search across all documents in the connected Craft space", - parameters: { + parameters: Schema.Struct({ query: Schema.String.annotate({ description: "Search query text" }), - }, + }), success: Schema.Unknown, }) export const CraftGetDocument = Tool.make("craft_get_document", { description: "Fetch the content blocks of a Craft document by its ID", - parameters: { + parameters: Schema.Struct({ documentId: Schema.String.annotate({ description: "The document ID to fetch" }), - }, + }), success: Schema.Unknown, }) export const CraftCreateDocument = Tool.make("craft_create_document", { description: "Create a new Craft document. Use this after confirming with the user what you will create.", - parameters: { + parameters: Schema.Struct({ title: Schema.String.annotate({ description: "Document title" }), content: Schema.optional( Schema.String.annotate({ description: "Initial text content for the document" }), @@ -27,13 +27,13 @@ export const CraftCreateDocument = Tool.make("craft_create_document", { folderId: Schema.optional( Schema.String.annotate({ description: "Optional folder ID to create the document in" }), ), - }, + }), success: Schema.Unknown, }) export const CraftInsertBlocks = Tool.make("craft_insert_blocks", { description: "Add content blocks to an existing Craft document", - parameters: { + parameters: Schema.Struct({ documentId: Schema.String.annotate({ description: "The document ID to add blocks to" }), blocks: Schema.Array( Schema.Struct({ @@ -46,31 +46,31 @@ export const CraftInsertBlocks = Tool.make("craft_insert_blocks", { parentBlockId: Schema.optional( Schema.String.annotate({ description: "Optional parent block ID to nest under" }), ), - }, + }), success: Schema.Unknown, }) export const CraftGetTasks = Tool.make("craft_get_tasks", { description: "List tasks from the connected Craft space", - parameters: { + parameters: Schema.Struct({ scope: Schema.optional( Schema.Literals(["inbox", "active", "upcoming", "logbook"]).annotate({ description: "Task scope filter (inbox, active, upcoming, or logbook)", }), ), - }, + }), success: Schema.Unknown, }) export const CraftCreateTask = Tool.make("craft_create_task", { description: "Create a task in the connected Craft space. Use this after confirming with the user what you will create.", - parameters: { + parameters: Schema.Struct({ content: Schema.String.annotate({ description: "Task content/description" }), documentId: Schema.optional( Schema.String.annotate({ description: "Optional document ID to associate the task with" }), ), - }, + }), success: Schema.Unknown, }) @@ -81,10 +81,10 @@ export const CraftGetFolders = Tool.make("craft_get_folders", { export const CraftSearchBlocks = Tool.make("craft_search_blocks", { description: "Search within a specific Craft document for matching blocks", - parameters: { + parameters: Schema.Struct({ documentId: Schema.String.annotate({ description: "The document ID to search within" }), query: Schema.String.annotate({ description: "Search query text" }), - }, + }), success: Schema.Unknown, }) diff --git a/bots/hazel-bot/src/tools/linear.ts b/bots/hazel-bot/src/tools/linear.ts index 7f1da0732..253a9d118 100644 --- a/bots/hazel-bot/src/tools/linear.ts +++ b/bots/hazel-bot/src/tools/linear.ts @@ -18,7 +18,7 @@ export const LinearGetDefaultTeam = Tool.make("linear_get_default_team", { export const LinearCreateIssue = Tool.make("linear_create_issue", { description: "Create a Linear issue. Use this after confirming with the user what you will create.", - parameters: { + parameters: Schema.Struct({ title: Schema.String.annotate({ description: "Issue title (max ~80 chars recommended)", }), @@ -30,22 +30,22 @@ export const LinearCreateIssue = Tool.make("linear_create_issue", { description: "Optional team ID; if omitted, uses the user's default team", }), ), - }, + }), success: Schema.Struct({ issue: Schema.Unknown }), }) export const LinearFetchIssue = Tool.make("linear_fetch_issue", { description: 'Fetch a Linear issue by key (e.g. "ENG-123")', - parameters: { + parameters: Schema.Struct({ issueKey: Schema.String.annotate({ description: 'Issue key like "ENG-123"' }), - }, + }), success: Schema.Struct({ issue: Schema.Unknown }), }) export const LinearListIssues = Tool.make("linear_list_issues", { description: "List Linear issues with optional filters (team, state, assignee, priority). Returns paginated results.", - parameters: { + parameters: Schema.Struct({ teamId: Schema.optional(Schema.String.annotate({ description: "Filter by team ID" })), stateType: Schema.optional( Schema.Literals(["triage", "backlog", "unstarted", "started", "completed", "canceled"]).annotate({ @@ -64,13 +64,13 @@ export const LinearListIssues = Tool.make("linear_list_issues", { }), ), after: Schema.optional(Schema.String.annotate({ description: "Pagination cursor for next page" })), - }, + }), success: Schema.Unknown, }) export const LinearSearchIssues = Tool.make("linear_search_issues", { description: "Search Linear issues by text query. Searches across title, description, and comments.", - parameters: { + parameters: Schema.Struct({ query: Schema.String.annotate({ description: "Search text to find issues" }), first: Schema.optional( Schema.Number.annotate({ @@ -83,7 +83,7 @@ export const LinearSearchIssues = Tool.make("linear_search_issues", { description: "Include archived issues in search (default false)", }), ), - }, + }), success: Schema.Unknown, }) @@ -95,16 +95,16 @@ export const LinearListTeams = Tool.make("linear_list_teams", { export const LinearGetWorkflowStates = Tool.make("linear_get_workflow_states", { description: "Get available workflow states (statuses) from Linear. Optionally filter by team. Use this to find valid state IDs before updating issues.", - parameters: { + parameters: Schema.Struct({ teamId: Schema.optional(Schema.String.annotate({ description: "Filter states by team ID" })), - }, + }), success: Schema.Unknown, }) export const LinearUpdateIssue = Tool.make("linear_update_issue", { description: "Update an existing Linear issue. Use this after confirming with the user what changes to make. First use linear_get_workflow_states to get valid state IDs if changing status.", - parameters: { + parameters: Schema.Struct({ issueId: Schema.String.annotate({ description: 'Issue identifier (e.g., "ENG-123" or UUID)', }), @@ -125,7 +125,7 @@ export const LinearUpdateIssue = Tool.make("linear_update_issue", { description: "New priority (0=None, 1=Urgent, 2=High, 3=Medium, 4=Low)", }), ), - }, + }), success: Schema.Unknown, }) diff --git a/bots/hazel-bot/src/tools/toolkit.ts b/bots/hazel-bot/src/tools/toolkit.ts index 24c692018..0008cd189 100644 --- a/bots/hazel-bot/src/tools/toolkit.ts +++ b/bots/hazel-bot/src/tools/toolkit.ts @@ -107,26 +107,29 @@ const baseHandlers = { const buildLinearHandlers = (options: { bot: HazelBotClient; orgId: OrganizationId }) => { const getLinearToken = () => - options.bot.integration.getToken(options.orgId, "linear").pipe(Effect.map((r) => r.accessToken)) + (options.bot as any).integration.getToken(options.orgId, "linear").pipe(Effect.map((r: any) => r.accessToken)) return { linear_get_account_info: () => Effect.gen(function* () { const accessToken = yield* getLinearToken() - return yield* LinearApiClient.getAccountInfo(accessToken) + const client = yield* LinearApiClient + return yield* client.getAccountInfo(accessToken) }), linear_get_default_team: () => Effect.gen(function* () { const accessToken = yield* getLinearToken() - const team = yield* LinearApiClient.getDefaultTeam(accessToken) + const client = yield* LinearApiClient + const team = yield* client.getDefaultTeam(accessToken) return { team } }), linear_create_issue: (args: { title: string; description?: string; teamId?: string }) => Effect.gen(function* () { const accessToken = yield* getLinearToken() - const issue = yield* LinearApiClient.createIssue(accessToken, { + const client = yield* LinearApiClient + const issue = yield* client.createIssue(accessToken, { title: args.title, description: args.description, teamId: args.teamId, @@ -137,7 +140,8 @@ const buildLinearHandlers = (options: { bot: HazelBotClient; orgId: Organization linear_fetch_issue: (args: { issueKey: string }) => Effect.gen(function* () { const accessToken = yield* getLinearToken() - const issue = yield* LinearApiClient.fetchIssue(args.issueKey, accessToken) + const client = yield* LinearApiClient + const issue = yield* client.fetchIssue(args.issueKey, accessToken) return { issue } }), @@ -204,9 +208,9 @@ const buildLinearHandlers = (options: { bot: HazelBotClient; orgId: Organization const buildCraftHandlers = (options: { bot: HazelBotClient; orgId: OrganizationId }) => { const getCraftCredentials = () => - options.bot.integration.getToken(options.orgId, "craft").pipe( - Effect.map((r) => ({ - accessToken: r.accessToken, + (options.bot as any).integration.getToken(options.orgId, "craft").pipe( + Effect.map((r: any) => ({ + accessToken: r.accessToken as string, baseUrl: (r.settings as Record | null)?.baseUrl as string, })), ) @@ -215,19 +219,22 @@ const buildCraftHandlers = (options: { bot: HazelBotClient; orgId: OrganizationI craft_search_documents: (args: { query: string }) => Effect.gen(function* () { const { baseUrl, accessToken } = yield* getCraftCredentials() - return yield* CraftApiClient.searchDocuments(baseUrl, accessToken, args.query) + const client = yield* CraftApiClient + return yield* client.searchDocuments(baseUrl, accessToken, args.query) }), craft_get_document: (args: { documentId: string }) => Effect.gen(function* () { const { baseUrl, accessToken } = yield* getCraftCredentials() - return yield* CraftApiClient.getBlocks(baseUrl, accessToken, args.documentId) + const client = yield* CraftApiClient + return yield* client.getBlocks(baseUrl, accessToken, args.documentId) }), craft_create_document: (args: { title: string; content?: string; folderId?: string }) => Effect.gen(function* () { const { baseUrl, accessToken } = yield* getCraftCredentials() - return yield* CraftApiClient.createDocuments(baseUrl, accessToken, [ + const client = yield* CraftApiClient + return yield* client.createDocuments(baseUrl, accessToken, [ { title: args.title, content: args.content, folderId: args.folderId }, ]) }), @@ -239,7 +246,8 @@ const buildCraftHandlers = (options: { bot: HazelBotClient; orgId: OrganizationI }) => Effect.gen(function* () { const { baseUrl, accessToken } = yield* getCraftCredentials() - return yield* CraftApiClient.insertBlocks( + const client = yield* CraftApiClient + return yield* client.insertBlocks( baseUrl, accessToken, args.documentId, @@ -251,13 +259,15 @@ const buildCraftHandlers = (options: { bot: HazelBotClient; orgId: OrganizationI craft_get_tasks: (args: { scope?: "inbox" | "active" | "upcoming" | "logbook" }) => Effect.gen(function* () { const { baseUrl, accessToken } = yield* getCraftCredentials() - return yield* CraftApiClient.getTasks(baseUrl, accessToken, args.scope) + const client = yield* CraftApiClient + return yield* client.getTasks(baseUrl, accessToken, args.scope) }), craft_create_task: (args: { content: string; documentId?: string }) => Effect.gen(function* () { const { baseUrl, accessToken } = yield* getCraftCredentials() - return yield* CraftApiClient.createTasks(baseUrl, accessToken, [ + const client = yield* CraftApiClient + return yield* client.createTasks(baseUrl, accessToken, [ { content: args.content, documentId: args.documentId }, ]) }), @@ -265,13 +275,15 @@ const buildCraftHandlers = (options: { bot: HazelBotClient; orgId: OrganizationI craft_get_folders: () => Effect.gen(function* () { const { baseUrl, accessToken } = yield* getCraftCredentials() - return yield* CraftApiClient.listFolders(baseUrl, accessToken) + const client = yield* CraftApiClient + return yield* client.listFolders(baseUrl, accessToken) }), craft_search_blocks: (args: { documentId: string; query: string }) => Effect.gen(function* () { const { baseUrl, accessToken } = yield* getCraftCredentials() - return yield* CraftApiClient.searchBlocks(baseUrl, accessToken, args.documentId, args.query) + const client = yield* CraftApiClient + return yield* client.searchBlocks(baseUrl, accessToken, args.documentId, args.query) }), } as const } @@ -289,41 +301,37 @@ export const buildToolkit = (options: { const hasCraft = options.enabledIntegrations.has("craft") if (hasLinear && hasCraft) { + const handlers = { + ...baseHandlers, + ...buildLinearHandlers(options), + ...buildCraftHandlers(options), + } return Effect.gen(function* () { - const handlers = { - ...baseHandlers, - ...buildLinearHandlers(options), - ...buildCraftHandlers(options), - } - const ctx = yield* LinearAndCraftToolkit.toContext(handlers as any) - return yield* Effect.provide(LinearAndCraftToolkit, ctx) - }) + return yield* LinearAndCraftToolkit + }).pipe(Effect.provide(LinearAndCraftToolkit.toLayer(handlers as any))) } if (hasLinear) { + const handlers = { + ...baseHandlers, + ...buildLinearHandlers(options), + } return Effect.gen(function* () { - const handlers = { - ...baseHandlers, - ...buildLinearHandlers(options), - } - const ctx = yield* LinearToolkit.toContext(handlers as any) - return yield* Effect.provide(LinearToolkit, ctx) - }) + return yield* LinearToolkit + }).pipe(Effect.provide(LinearToolkit.toLayer(handlers as any))) } if (hasCraft) { + const handlers = { + ...baseHandlers, + ...buildCraftHandlers(options), + } return Effect.gen(function* () { - const handlers = { - ...baseHandlers, - ...buildCraftHandlers(options), - } - const ctx = yield* CraftToolkit.toContext(handlers as any) - return yield* Effect.provide(CraftToolkit, ctx) - }) + return yield* CraftToolkit + }).pipe(Effect.provide(CraftToolkit.toLayer(handlers as any))) } return Effect.gen(function* () { - const ctx = yield* BaseToolkit.toContext(baseHandlers) - return yield* Effect.provide(BaseToolkit, ctx) - }) + return yield* BaseToolkit + }).pipe(Effect.provide(BaseToolkit.toLayer(baseHandlers))) } diff --git a/bots/linear-bot/src/index.ts b/bots/linear-bot/src/index.ts index 761c1dcd7..ff495bb52 100644 --- a/bots/linear-bot/src/index.ts +++ b/bots/linear-bot/src/index.ts @@ -28,7 +28,9 @@ runHazelBot({ layers: [LinearApiClient.layer], setup: (bot) => Effect.gen(function* () { - yield* bot.onCommand(IssueCommand, (ctx) => + const botAny = bot as any + + yield* botAny.onCommand(IssueCommand, (ctx: any) => Effect.gen(function* () { yield* Effect.log(`Received /issue command from ${ctx.userId}`) @@ -36,37 +38,38 @@ runHazelBot({ yield* Effect.log(`Creating Linear issue: ${title}`) - const { accessToken } = yield* bot.integration.getToken(ctx.orgId, "linear") + const { accessToken } = yield* botAny.integration.getToken(ctx.orgId, "linear") - const issue = yield* LinearApiClient.createIssue(accessToken, { + const linearClient = yield* LinearApiClient + const issue = yield* linearClient.createIssue(accessToken, { title, description, }) yield* Effect.log(`Created Linear issue: ${issue.identifier}`) - yield* bot.message.send( + yield* botAny.message.send( ctx.channelId, `@[userId:${ctx.userId}] created an issue: ${issue.url}`, ) - }).pipe(bot.withErrorHandler(ctx)), + }).pipe(botAny.withErrorHandler(ctx)), ) - yield* bot.onCommand(IssueifyCommand, (ctx) => + yield* botAny.onCommand(IssueifyCommand, (ctx: any) => Effect.gen(function* () { yield* Effect.log(`Received /issueify command from ${ctx.userId}`) // Fetch messages from the channel - const { data: messages } = yield* bot.message.list(ctx.channelId, { + const { data: messages } = yield* botAny.message.list(ctx.channelId, { limit: 20, }) if (messages.length === 0) { - yield* bot.message.send(ctx.channelId, "No messages found in this channel.") + yield* botAny.message.send(ctx.channelId, "No messages found in this channel.") return } - const stream = yield* bot.ai.stream(ctx.channelId, { + const stream = yield* botAny.ai.stream(ctx.channelId, { model: "moonshotai/kimi-k2.5 (agent)", loading: { text: "Thinking...", @@ -83,7 +86,7 @@ runHazelBot({ // Format messages for AI analysis const conversationText = chronologicalMessages - .map((msg) => { + .map((msg: any) => { const timestamp = new Date(msg.createdAt).toLocaleString() const content = msg.content.replace(/@\[userId:[^\]]+\]/g, "@user") return `[${timestamp}] User: ${content}` @@ -117,15 +120,16 @@ ${conversationText}`, const text = response.text // Parse the JSON response - const generatedIssue = yield* Schema.decodeUnknown(GeneratedIssueSchema)(JSON.parse(text)) + const generatedIssue = yield* Schema.decodeUnknownEffect(GeneratedIssueSchema)(JSON.parse(text)) yield* Effect.log(`Generated issue: ${generatedIssue.title}`) yield* stream.setText("📝 Creating Linear issue...") - const { accessToken } = yield* bot.integration.getToken(ctx.orgId, "linear") + const { accessToken } = yield* botAny.integration.getToken(ctx.orgId, "linear") - const issue = yield* LinearApiClient.createIssue(accessToken, { + const linearClient = yield* LinearApiClient + const issue = yield* linearClient.createIssue(accessToken, { title: generatedIssue.title, description: generatedIssue.description, }) @@ -137,7 +141,7 @@ ${conversationText}`, `@[userId:${ctx.userId}] created an issue from this conversation: ${issue.url}`, ) yield* stream.complete() - }).pipe(bot.withErrorHandler(ctx), Effect.provide(OpenRouterModelLayer)), + }).pipe(botAny.withErrorHandler(ctx), Effect.provide(OpenRouterModelLayer)), ) }), }) diff --git a/bun.lock b/bun.lock index ff01049f5..b6df7934d 100644 --- a/bun.lock +++ b/bun.lock @@ -47,7 +47,7 @@ "@hazel/integrations": "workspace:*", "@hazel/schema": "workspace:*", "@workos-inc/node": "^7.77.0", - "dfx": "^0.127.0", + "dfx": "^1.0.10", "drizzle-orm": "^0.45.1", "effect": "catalog:effect", "jose": "^6.1.3", @@ -901,7 +901,7 @@ "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.32", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.32" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-NsR7vJBXDbYr+8fcAaKUyGC022s7lTeUODPD8kabqOlVrs86k/LNcTtVtGXUAjY4o1u/c3kipJ0p4z9rCniwsQ=="], - "@effect/platform": ["@effect/platform@0.94.3", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.16" } }, "sha512-bvTR8xLQoRpKgHuprZDOeQdPkhyVw+WT05iI9jl2s8Qiblyk5Dz2JLwJU+EFeksIBaPYz49xa635Om91T1CefQ=="], + "@effect/platform": ["@effect/platform@0.94.5", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.17" } }, "sha512-z05APUiDDPbodhTkH/RJqOLoCU11bU2IZLfcwLFrld03+ob1VeqRnELQlmueLIYm6NZifHAtjl32V+GRt34y4A=="], "@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.32", "", { "dependencies": { "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^4.0.0-beta.32" } }, "sha512-sV5d/Qs6y74DOcIQe4QaCrNhqVwRvZfi13cQAMmnv7xX+El7TDn0EZ0i8xmT4mCF2KrG3mHKHCwKoDOIHgEcxg=="], @@ -2569,13 +2569,13 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "dfx": ["dfx@0.127.0", "", { "dependencies": { "discord-api-types": "^0.38.33" }, "optionalDependencies": { "discord-verify": "^1.2.0" }, "peerDependencies": { "@effect/platform": "^0.93", "effect": "^3.19" } }, "sha512-WQGzD80m8Jx0Kn8lqbrjTrMINUC+mTgU8C+4Kv6yxZc8ZK02rhqkeiBhdMzNDXQX8c/gr3R9GcUYoBJ31wQBoA=="], + "dfx": ["dfx@1.0.10", "", { "dependencies": { "discord-api-types": "^0.38.40" }, "optionalDependencies": { "discord-verify": "^1.2.0" }, "peerDependencies": { "effect": "4.0.0-beta.22" } }, "sha512-m0BX8PpMLdSFYoKQz5IZzhDhqs0FsVaQx4PqtV3/u/mx6Rgo14m8PdFrWTjPnYH+W7GW6JxkBqN9c9tUNDidGQ=="], "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "direction": ["direction@1.0.4", "", { "bin": { "direction": "cli.js" } }, "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ=="], - "discord-api-types": ["discord-api-types@0.38.38", "", {}, "sha512-7qcM5IeZrfb+LXW07HvoI5L+j4PQeMZXEkSm1htHAHh4Y9JSMXBWjy/r7zmUCOj4F7zNjMcm7IMWr131MT2h0Q=="], + "discord-api-types": ["discord-api-types@0.38.42", "", {}, "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ=="], "discord-verify": ["discord-verify@1.2.0", "", { "dependencies": { "@types/express": "^4.17.17" } }, "sha512-8qlrMROW8DhpzWWzgNq9kpeLDxKanWa4EDVoj/ASVv2nr+dSr4JPmu2tFSydf3hAGI/OIJTnZyD0JulMYIxx4w=="], @@ -3469,9 +3469,9 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], + "pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], - "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], + "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], "pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="], @@ -3483,7 +3483,7 @@ "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="], - "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], + "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], "pg-types": ["pg-types@4.1.0", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg=="], @@ -4421,14 +4421,10 @@ "@effect/platform-node-shared/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "@effect/rpc/@effect/platform": ["@effect/platform@0.94.5", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.17" } }, "sha512-z05APUiDDPbodhTkH/RJqOLoCU11bU2IZLfcwLFrld03+ob1VeqRnELQlmueLIYm6NZifHAtjl32V+GRt34y4A=="], - "@effect/rpc/effect": ["effect@3.19.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg=="], "@effect/rpc/msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], - "@effect/sql-pg/pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@esbuild-kit/esm-loader/get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], @@ -4635,6 +4631,8 @@ "@types/koa/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], + "@types/pg/pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], + "@types/pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], "@types/send/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], @@ -4707,8 +4705,6 @@ "cssstyle/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], - "dfx/effect": ["effect@3.19.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg=="], - "docker-modem/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "dockerode/tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], @@ -4793,9 +4789,7 @@ "parse5-parser-stream/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - "pg/pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="], - - "pg/pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], + "pg/pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="], "pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], @@ -5121,14 +5115,6 @@ "@effect/rpc/effect/fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], - "@effect/sql-pg/pg/pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], - - "@effect/sql-pg/pg/pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="], - - "@effect/sql-pg/pg/pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], - - "@effect/sql-pg/pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -5485,8 +5471,6 @@ "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], - "dfx/effect/fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], - "dockerode/tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], "dockerode/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], @@ -6009,14 +5993,6 @@ "@effect/rpc/effect/fast-check/pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], - "@effect/sql-pg/pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], - - "@effect/sql-pg/pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="], - - "@effect/sql-pg/pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], - - "@effect/sql-pg/pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "@grpc/grpc-js/@grpc/proto-loader/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "@grpc/grpc-js/@grpc/proto-loader/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -6273,8 +6249,6 @@ "babel-dead-code-elimination/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "dfx/effect/fast-check/pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], - "dockerode/tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "effect-rpc-tanstack-devtools/effect/fast-check/pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], diff --git a/libs/bot-sdk/src/auth.ts b/libs/bot-sdk/src/auth.ts index 8dd149431..f8799b83e 100644 --- a/libs/bot-sdk/src/auth.ts +++ b/libs/bot-sdk/src/auth.ts @@ -57,7 +57,7 @@ export class BotAuth extends ServiceMap.Service()("BotAuth", { } }), }) { - static readonly layer = Layer.effect(this, this.make) + static readonly layer = (context: BotAuthContext) => Layer.effect(this, this.make(context)) } /** @@ -85,8 +85,8 @@ export const createAuthContextFromToken = ( Effect.retry( Schedule.exponential("1 second", 2).pipe( Schedule.jittered, - Schedule.while((duration) => - Duration.isLessThanOrEqualTo(duration, Duration.seconds(30)), + Schedule.while((metadata) => + Duration.isLessThanOrEqualTo(metadata.output, Duration.seconds(30)), ), ), ), diff --git a/libs/bot-sdk/src/bot-config.ts b/libs/bot-sdk/src/bot-config.ts index fa9d19e2d..cdeb6c600 100644 --- a/libs/bot-sdk/src/bot-config.ts +++ b/libs/bot-sdk/src/bot-config.ts @@ -5,7 +5,7 @@ * Provides automatic validation and helpful error messages. */ -import { Config } from "effect" +import { Config, type Effect } from "effect" const DEFAULT_ACTORS_URL = "https://rivet.hazel.sh" @@ -28,4 +28,4 @@ export const BotEnvConfig = Config.all({ healthPort: Config.number("PORT").pipe(Config.withDefault(0)), }) -export type BotEnvConfig = typeof BotEnvConfig extends Config ? T : never +export type BotEnvConfig = Effect.Success diff --git a/libs/bot-sdk/src/hazel-bot-sdk.ts b/libs/bot-sdk/src/hazel-bot-sdk.ts index 236bca1a4..8ee302537 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.ts @@ -266,7 +266,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB capacity: 100, timeToLive: Duration.seconds(30), lookup: (orgId: OrganizationId) => - httpApiClient["bot-commands"].getEnabledIntegrations({ path: { orgId } }).pipe( + httpApiClient["bot-commands"].getEnabledIntegrations({ params: { orgId } }).pipe( Effect.map((r) => new Set(r.providers)), Effect.withSpan("bot.integration.getEnabled", { attributes: { orgId } }), ), @@ -566,7 +566,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB const startWebSocketGatewayLoop = (runtimeConfig: HazelBotRuntimeConfig) => Effect.gen(function* () { - const runtime = yield* Effect.runtime() + const services = yield* Effect.services() let nextResumeOffset = yield* loadResumeOffset(runtimeConfig) let nextSessionId = yield* loadGatewaySessionId() let hasConnected = false @@ -664,7 +664,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB } case "READY": { sessionId = frame.sessionId - Runtime.runPromise(runtime)( + Effect.runPromiseWith(services)( setBotState( GATEWAY_SESSION_ID_STATE_KEY, Schema.String, @@ -700,7 +700,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB return } case "DISPATCH": { - Runtime.runPromise(runtime)( + Effect.runPromiseWith(services)( Effect.gen(function* () { yield* processGatewayBatch(frame.events) yield* gatewaySessionStore.save( @@ -741,7 +741,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB return } case "INVALID_SESSION": { - Runtime.runPromise(runtime)( + Effect.runPromiseWith(services)( Effect.gen(function* () { yield* gatewaySessionStore.save( authContext.botId as BotId, @@ -801,30 +801,28 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB }), }) - yield* Effect.forkScoped( - Effect.forever( - Effect.gen(function* () { - const nextState = yield* connectOnce.pipe( - Effect.catch((error) => - Effect.logWarning("Bot gateway websocket failed, retrying", { - error, - botId: authContext.botId, - offset: nextResumeOffset, + yield* Effect.forever( + Effect.gen(function* () { + const nextState = yield* connectOnce.pipe( + Effect.catch((error) => + Effect.logWarning("Bot gateway websocket failed, retrying", { + error, + botId: authContext.botId, + offset: nextResumeOffset, + sessionId: nextSessionId, + }).pipe( + Effect.andThen(Effect.sleep(Duration.seconds(1))), + Effect.as({ + resumeOffset: nextResumeOffset, sessionId: nextSessionId, - }).pipe( - Effect.andThen(Effect.sleep(Duration.seconds(1))), - Effect.as({ - resumeOffset: nextResumeOffset, - sessionId: nextSessionId, - }), - ), + }), ), - ) - nextResumeOffset = nextState.resumeOffset - nextSessionId = nextState.sessionId - }).pipe(Effect.andThen(Effect.sleep(Duration.millis(250)))), - ), - ) + ), + ) + nextResumeOffset = nextState.resumeOffset + nextSessionId = nextState.sessionId + }).pipe(Effect.andThen(Effect.sleep(Duration.millis(250)))), + ).pipe(Effect.forkScoped) }) const startGatewayLoop = () => @@ -1031,7 +1029,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB messageLimiter( httpApiClient["api-v1-messages"] .updateMessage({ - path: { id: messageId }, + params: { id: messageId }, payload: { content: payload.content, embeds: payload.embeds ?? null, @@ -1048,7 +1046,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB }), ), ), - ) + ) as any return { /** @@ -1231,7 +1229,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB messageLimiter( httpApiClient["api-v1-messages"] .updateMessage({ - path: { id: message.id }, + params: { id: message.id }, payload: { content }, }) .pipe( @@ -1258,7 +1256,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB messageLimiter( httpApiClient["api-v1-messages"] .deleteMessage({ - path: { id }, + params: { id }, }) .pipe( Effect.mapError( @@ -1282,7 +1280,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB messageLimiter( httpApiClient["api-v1-messages"] .toggleReaction({ - path: { id: message.id }, + params: { id: message.id }, payload: { emoji, channelId: message.channelId, @@ -1335,7 +1333,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB ) => httpApiClient["api-v1-messages"] .listMessages({ - urlParams: { + query: { channel_id: channelId, starting_after: options?.startingAfter, ending_before: options?.endingBefore, @@ -1371,8 +1369,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB description?: string | null }, ) => - rpc.channel - .update({ + rpc["channel.update"]({ id: channel.id, type: channel.type, organizationId: channel.organizationId, @@ -1381,7 +1378,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB ...updates, }) .pipe( - Effect.map((r) => r.data), + Effect.map((r: any) => r.data), Effect.withSpan("bot.channel.update", { attributes: { channelId: channel.id } }), ), @@ -1395,8 +1392,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB * @throws MessageNotFoundError if the message doesn't exist */ createThread: (messageId: MessageId, channelId: ChannelId) => - rpc.channel - .createThread({ + rpc["channel.createThread"]({ messageId, }) .pipe( @@ -1408,7 +1404,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB cause, }), ), - Effect.map((r) => r.data), + Effect.map((r: any) => r.data), Effect.withSpan("bot.channel.createThread", { attributes: { messageId, channelId }, }), @@ -1425,14 +1421,13 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB * @param memberId - Channel member ID */ start: (channelId: ChannelId, memberId: ChannelMemberId) => - rpc.typingIndicator - .create({ + rpc["typingIndicator.create"]({ channelId, memberId, lastTyped: Date.now(), }) .pipe( - Effect.map((r) => r.data), + Effect.map((r: any) => r.data), Effect.withSpan("bot.typing.start", { attributes: { channelId, memberId } }), ), @@ -1441,12 +1436,11 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB * @param id - Typing indicator ID */ stop: (id: TypingIndicatorId) => - rpc.typingIndicator - .delete({ + rpc["typingIndicator.delete"]({ id, }) .pipe( - Effect.map((r) => r.data), + Effect.map((r: any) => r.data), Effect.withSpan("bot.typing.stop", { attributes: { typingIndicatorId: id } }), ), }, @@ -1471,7 +1465,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB * ``` */ getToken: (orgId: OrganizationId, provider: IntegrationConnection.IntegrationProvider) => - httpApiClient["bot-commands"].getIntegrationToken({ path: { orgId, provider } }).pipe( + httpApiClient["bot-commands"].getIntegrationToken({ params: { orgId, provider } }).pipe( Effect.withSpan("bot.integration.getToken", { attributes: { orgId, provider }, }), @@ -1493,7 +1487,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB * } * ``` */ - getEnabled: (orgId: OrganizationId) => enabledIntegrationsCache.get(orgId), + getEnabled: (orgId: OrganizationId) => Cache.get(enabledIntegrationsCache, orgId), /** * Invalidate the enabled integrations cache for an organization. @@ -1501,7 +1495,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB * * @param orgId - The organization ID to invalidate cache for */ - invalidateCache: (orgId: OrganizationId) => enabledIntegrationsCache.invalidate(orgId), + invalidateCache: (orgId: OrganizationId) => Cache.invalidate(enabledIntegrationsCache, orgId), }, /** @@ -1559,7 +1553,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB (ctx: TypedCommandContext) => ( effect: Effect.Effect, - ): Effect.Effect => + ) => effect.pipe( Effect.mapError( (cause) => @@ -1660,7 +1654,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB const actorsService = yield* createActorsServiceFn() return yield* createStreamSessionInternal( - createMessageFnHelper, + createMessageFnHelper as any, updateMessageFnHelper, actorsService, channelId, @@ -1695,7 +1689,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB const actorsService = yield* createActorsServiceFn() return yield* createAIStreamSessionInternal( - createMessageFnHelper, + createMessageFnHelper as any, updateMessageFnHelper, actorsService, channelId, @@ -1730,7 +1724,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB */ withErrorHandler: (ctx: TypedCommandContext, session: AIStreamSession) => - (effect: Effect.Effect): Effect.Effect => + (effect: Effect.Effect) => effect.pipe( Effect.mapError( (cause) => @@ -1954,7 +1948,7 @@ export const createHazelBot = = EmptyCommand process.env.RIVET_URL ?? DEFAULT_ACTORS_ENDPOINT - const AuthLayer = Layer.unwrapEffect( + const AuthLayer = Layer.unwrap( createAuthContextFromToken(config.botToken, backendUrl).pipe( Effect.map((context) => BotAuth.layer(context)), ), @@ -1993,7 +1987,7 @@ export const createHazelBot = = EmptyCommand // Create logger layer with configurable level and format // Defaults: INFO level, format based on NODE_ENV // LOG_LEVEL env var overrides config (e.g. LOG_LEVEL=debug bun run dev) - const LoggerLayer = Layer.unwrapEffect( + const LoggerLayer = Layer.unwrap( Effect.gen(function* () { const nodeEnv = yield* Config.string("NODE_ENV").pipe(Config.withDefault("development")) const envLogLevel = yield* Config.string("LOG_LEVEL").pipe(Config.withDefault("")) @@ -2001,7 +1995,7 @@ export const createHazelBot = = EmptyCommand const resolvedLevel = envLogLevel ? logLevelFromString(envLogLevel) - : (config.logging?.level ?? LogLevel.Info) + : (config.logging?.level ?? "Info") const logConfig: BotLogConfig = { level: resolvedLevel, @@ -2033,5 +2027,5 @@ export const createHazelBot = = EmptyCommand ).pipe(Layer.provide(AuthLayer), Layer.provide(LoggerLayer), Layer.provide(TracingLayer)) // Create runtime - return ManagedRuntime.make(AllLayers) + return ManagedRuntime.make(AllLayers as Layer.Layer) } diff --git a/libs/bot-sdk/src/log-config.ts b/libs/bot-sdk/src/log-config.ts index dca5ca654..29ebc381f 100644 --- a/libs/bot-sdk/src/log-config.ts +++ b/libs/bot-sdk/src/log-config.ts @@ -4,7 +4,7 @@ * Provides configurable log levels and formats for bot logging. */ -import { Layer, Logger, LogLevel } from "effect" +import { Effect, Layer, Logger, LogLevel, References } from "effect" /** * Log output format options @@ -93,9 +93,12 @@ export const debugLogConfig: BotLogConfig = { * Create a logger layer from log configuration */ export const createLoggerLayer = (config: BotLogConfig): Layer.Layer => { - const formatLayer = config.format === "structured" ? Logger.structured : Logger.pretty + const logger = config.format === "structured" ? Logger.consoleStructured : Logger.consolePretty() - return Layer.mergeAll(formatLayer, Logger.minimumLogLevel(config.level)) + return Layer.mergeAll( + Logger.layer([logger]), + Layer.succeed(References.MinimumLogLevel, config.level), + ) } /** diff --git a/libs/bot-sdk/src/log-context.ts b/libs/bot-sdk/src/log-context.ts index 4b786b806..0f08225c8 100644 --- a/libs/bot-sdk/src/log-context.ts +++ b/libs/bot-sdk/src/log-context.ts @@ -6,7 +6,7 @@ */ import type { ChannelId, OrganizationId, UserId } from "@hazel/schema" -import { Effect, FiberRef, type Tracer } from "effect" +import { Effect, ServiceMap, type Tracer } from "effect" type GatewayEventType = string type GatewayEventOperation = string @@ -52,12 +52,12 @@ export interface LogContext { } /** - * FiberRef for fiber-local context propagation + * Reference for fiber-local context propagation * Allows context to flow through Effect operations without explicit passing */ -export const currentLogContext: FiberRef.FiberRef = FiberRef.unsafeMake( - null, -) +export const currentLogContext = ServiceMap.Reference("@hazel/bot-sdk/CurrentLogContext", { + defaultValue: () => null, +}) /** * Generate a unique correlation ID @@ -206,17 +206,19 @@ export const withLogContext = ( make: Effect.Effect, options?: { readonly parent?: Tracer.AnySpan }, ): Effect.Effect => - effect.pipe( + make.pipe( Effect.annotateLogs(contextToAnnotations(ctx)), Effect.withSpan(spanName, { attributes: contextToSpanAttributes(ctx), ...options }), - (eff) => Effect.locally(eff, currentLogContext, ctx), + Effect.provideService(currentLogContext, ctx), ) /** * Get the current log context from the FiberRef * Returns null if no context is set */ -export const getLogContext: Effect.Effect = FiberRef.get(currentLogContext) +export const getLogContext: Effect.Effect = Effect.gen(function* () { + return yield* currentLogContext +}) /** * Get the current correlation ID from context diff --git a/libs/bot-sdk/src/retry.ts b/libs/bot-sdk/src/retry.ts index 099c729fb..78feba9ae 100644 --- a/libs/bot-sdk/src/retry.ts +++ b/libs/bot-sdk/src/retry.ts @@ -21,7 +21,8 @@ export const RetryStrategy = { */ transientErrors: Schedule.exponential(Duration.millis(100), 2).pipe( Schedule.jittered, - Schedule.whileInput((error) => { + Schedule.while((metadata) => { + const error = metadata.input const tag = typeof error === "object" && error !== null && "_tag" in error ? String((error as Record)["_tag"]) @@ -42,7 +43,8 @@ export const RetryStrategy = { */ connectionErrors: Schedule.exponential(Duration.seconds(1), 2).pipe( Schedule.jittered, - Schedule.whileInput((error) => { + Schedule.while((metadata) => { + const error = metadata.input const tag = typeof error === "object" && error !== null && "_tag" in error ? String((error as Record)["_tag"]) @@ -60,7 +62,8 @@ export const RetryStrategy = { */ quickRetry: Schedule.exponential(Duration.millis(50), 2).pipe( Schedule.jittered, - Schedule.whileInput((error) => { + Schedule.while((metadata) => { + const error = metadata.input const tag = typeof error === "object" && error !== null && "_tag" in error ? String((error as Record)["_tag"]) @@ -78,15 +81,16 @@ export const RetryStrategy = { schedule: Schedule.Schedule, options: { readonly threshold?: number - readonly resetAfter?: Duration.DurationInput + readonly resetAfter?: Duration.Input } = {}, ) => { const threshold = options.threshold ?? 5 - const resetAfter = options.resetAfter ?? Duration.seconds(30) return schedule.pipe( - Schedule.resetAfter(resetAfter), - Schedule.whileOutput((attempt) => (typeof attempt === "number" ? attempt < threshold : true)), + Schedule.while((metadata) => { + const attempt = metadata.output + return typeof attempt === "number" ? attempt < threshold : true + }), ) }, diff --git a/libs/bot-sdk/src/rpc/auth-middleware.ts b/libs/bot-sdk/src/rpc/auth-middleware.ts index e27a4f53a..0d0cfee31 100644 --- a/libs/bot-sdk/src/rpc/auth-middleware.ts +++ b/libs/bot-sdk/src/rpc/auth-middleware.ts @@ -28,8 +28,8 @@ import { Effect } from "effect" * ``` */ export const createBotAuthMiddleware = (botToken: string) => - RpcMiddleware.layerClient(AuthMiddleware, ({ request }) => - Effect.succeed({ + RpcMiddleware.layerClient(AuthMiddleware, ({ request, next }) => + next({ ...request, headers: Headers.set(request.headers ?? Headers.empty, "authorization", `Bearer ${botToken}`), }), diff --git a/libs/bot-sdk/src/rpc/client.ts b/libs/bot-sdk/src/rpc/client.ts index eeee2738c..076e6839f 100644 --- a/libs/bot-sdk/src/rpc/client.ts +++ b/libs/bot-sdk/src/rpc/client.ts @@ -46,7 +46,7 @@ export class BotRpcClientConfigTag extends ServiceMap.Service> + Effect.Success> >()("@hazel/bot-sdk/BotRpcClient") {} /** diff --git a/libs/bot-sdk/src/run-bot.ts b/libs/bot-sdk/src/run-bot.ts index ba41375df..1144db188 100644 --- a/libs/bot-sdk/src/run-bot.ts +++ b/libs/bot-sdk/src/run-bot.ts @@ -131,7 +131,7 @@ export const runHazelBot = = EmptyCommands>( const bot = yield* HazelBotClient // Run user's setup function - yield* options.setup(bot) + yield* options.setup(bot as any) // Start the bot yield* bot.start diff --git a/libs/bot-sdk/src/services/health-server.ts b/libs/bot-sdk/src/services/health-server.ts index 2cae45dcb..aa466f858 100644 --- a/libs/bot-sdk/src/services/health-server.ts +++ b/libs/bot-sdk/src/services/health-server.ts @@ -7,7 +7,7 @@ * Enabled by default on port 9090. Set `healthPort: false` in config to disable. */ -import { Effect, Layer, Runtime, ServiceMap } from "effect" +import { Effect, Layer, ServiceMap } from "effect" export interface BotHealthServerConfig { readonly port: number @@ -28,7 +28,7 @@ export class BotHealthServer extends ServiceMap.Service()("BotH make: Effect.gen(function* () { const config = yield* BotHealthServerConfigTag const startTime = Date.now() - const runtime = yield* Effect.runtime() + const services = yield* Effect.services() const collectHealth = Effect.sync( (): HealthResponse => ({ @@ -45,8 +45,8 @@ export class BotHealthServer extends ServiceMap.Service()("BotH fetch(req) { const url = new URL(req.url) if (req.method === "GET" && url.pathname === "/health") { - return Runtime.runPromise(runtime)(collectHealth).then( - (health) => + return Effect.runPromiseWith(services)(collectHealth).then( + (health: HealthResponse) => new Response(JSON.stringify(health), { status: 200, headers: { "Content-Type": "application/json" }, diff --git a/libs/bot-sdk/src/streaming/actors-client.ts b/libs/bot-sdk/src/streaming/actors-client.ts index 3de6d7d80..8b3095460 100644 --- a/libs/bot-sdk/src/streaming/actors-client.ts +++ b/libs/bot-sdk/src/streaming/actors-client.ts @@ -8,7 +8,7 @@ */ import { createActorsClient } from "@hazel/actors/client" -import { ServiceMap, Effect } from "effect" +import { ServiceMap, Effect, Layer } from "effect" // biome-ignore lint/suspicious/noExplicitAny: Opaque type to avoid non-portable DTS references to @hazel/actors internals export type MessageActor = any @@ -94,5 +94,5 @@ export class ActorsClient extends ServiceMap.Service()("@hazel/bot } as ActorsClientService }), }) { - static readonly layer = Layer.effect(this, this.make) + static readonly layer = (config: ActorsClientConfig) => Layer.effect(this, this.make(config)) } diff --git a/libs/bot-sdk/src/streaming/types.ts b/libs/bot-sdk/src/streaming/types.ts index 59385e9c6..932f94857 100644 --- a/libs/bot-sdk/src/streaming/types.ts +++ b/libs/bot-sdk/src/streaming/types.ts @@ -131,7 +131,7 @@ export const ToolCallChunk = Schema.Struct({ type: Schema.Literal("tool_call"), id: Schema.String, name: Schema.String, - input: Schema.Record({ key: Schema.String, value: Schema.Unknown }), + input: Schema.Record(Schema.String, Schema.Unknown), }) export type ToolCallChunk = Schema.Schema.Type diff --git a/libs/effect-electric-db-collection/src/service.ts b/libs/effect-electric-db-collection/src/service.ts index 91b8fee6e..a40e5faa5 100644 --- a/libs/effect-electric-db-collection/src/service.ts +++ b/libs/effect-electric-db-collection/src/service.ts @@ -3,7 +3,7 @@ import type { StandardSchemaV1 } from "@standard-schema/spec" import type { Collection } from "@tanstack/db" import { createCollection } from "@tanstack/db" import type { Txid } from "@tanstack/electric-db-collection" -import { Context, Effect, Layer } from "effect" +import { ServiceMap, Effect, Layer } from "effect" import { effectElectricCollectionOptions } from "./collection" import { DeleteError, InsertError, type InvalidTxIdError, type TxIdTimeoutError, UpdateError } from "./errors" import type { EffectElectricCollectionConfig } from "./types" @@ -81,7 +81,7 @@ export interface ElectricCollectionService< /** * Create a tag for an Electric collection service. * - * This factory uses `Context.Tag` instead of `Effect.Service` because collection + * This factory uses `ServiceMap.Service` instead of `Effect.Service` because collection * instances are created at runtime with dynamic configuration (shape URLs, schemas, * handlers) that vary per application context. The tag pattern allows: * @@ -118,14 +118,14 @@ export interface ElectricCollectionService< */ export const ElectricCollection = , TKey extends string | number = string | number>( id: string, -): Context.Tag, ElectricCollectionService> => - Context.GenericTag>(`ElectricCollection<${id}>`) +): ServiceMap.Service, ElectricCollectionService> => + ServiceMap.Service>(`ElectricCollection<${id}>`) /** * Create a Layer for an Electric collection service */ export function makeElectricCollectionLayer( - tag: Context.Tag< + tag: ServiceMap.Service< ElectricCollectionService, string | number>, ElectricCollectionService, string | number> >, @@ -135,7 +135,7 @@ export function makeElectricCollectionLayer( ): Layer.Layer, string | number>, never, never> export function makeElectricCollectionLayer>( - tag: Context.Tag< + tag: ServiceMap.Service< ElectricCollectionService, ElectricCollectionService >, @@ -145,7 +145,7 @@ export function makeElectricCollectionLayer>( ): Layer.Layer, never, never> export function makeElectricCollectionLayer( - tag: Context.Tag, + tag: ServiceMap.Service, config: EffectElectricCollectionConfig, ): Layer.Layer { return Layer.succeed( diff --git a/packages/effect-bun/src/Telemetry.ts b/packages/effect-bun/src/Telemetry.ts index af0e9b859..de03a36dd 100644 --- a/packages/effect-bun/src/Telemetry.ts +++ b/packages/effect-bun/src/Telemetry.ts @@ -46,7 +46,7 @@ export const createTracingLayer = (otelServiceName: string) => ) } - return DevTools.layerSocket.pipe(Layer.provide(BunSocket.layerWebSocketConstructor)) + return DevTools.layerWebSocket().pipe(Layer.provide(BunSocket.layerWebSocketConstructor)) } const otelBaseUrl = yield* Config.string("OTEL_BASE_URL") diff --git a/packages/rivet-effect/src/runtime.ts b/packages/rivet-effect/src/runtime.ts index e7a353a9f..86a431493 100644 --- a/packages/rivet-effect/src/runtime.ts +++ b/packages/rivet-effect/src/runtime.ts @@ -51,7 +51,7 @@ export const getManagedRuntime = (context: unknown): AnyManagedRuntime | undefin * Only works when `R = never` (no unsatisfied requirements). */ const runWithCurrentRuntime = (effect: Effect.Effect): Promise => - DefaultRuntime.runPromise(effect).catch((error) => + Effect.runPromise(effect).catch((error: any) => Promise.reject( new RuntimeExecutionError({ message: "Failed to execute effect with current runtime", From 8e789a4fb0a530649ce263b78b3460142b54b81a Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 12:49:01 +0100 Subject: [PATCH 14/34] fix --- apps/backend/fix-static-accessors.mjs | 141 ----------------- apps/backend/fix-static-accessors2.mjs | 142 ------------------ apps/backend/src/index.ts | 8 +- apps/backend/src/lib/create-transactionId.ts | 14 +- apps/backend/src/lib/policy-utils.test.ts | 24 +-- .../src/policies/attachment-policy.test.ts | 32 ++-- apps/backend/src/policies/bot-policy.test.ts | 26 ++-- .../policies/channel-member-policy.test.ts | 42 +++--- .../src/policies/channel-policy.test.ts | 18 +-- .../integration-connection-policy.test.ts | 12 +- .../src/policies/invitation-policy.test.ts | 28 ++-- .../src/policies/message-policy.test.ts | 22 +-- .../policies/message-reaction-policy.test.ts | 18 +-- .../src/policies/notification-policy.test.ts | 26 ++-- .../organization-member-policy.test.ts | 24 +-- .../src/policies/organization-policy.test.ts | 20 +-- .../policies/pinned-message-policy.test.ts | 24 +-- .../src/policies/policy-test-helpers.ts | 8 +- .../policies/typing-indicator-policy.test.ts | 22 +-- .../src/routes/api-v1/messages.http.ts | 36 ++--- apps/backend/src/routes/auth.http.ts | 50 +++--- apps/backend/src/routes/bot-commands.http.ts | 24 +-- apps/backend/src/routes/chat-sync.http.ts | 62 ++++---- .../src/routes/incoming-webhooks.http.ts | 12 +- .../src/routes/integration-commands.http.ts | 4 +- .../src/routes/integration-resources.http.ts | 50 +++--- .../src/routes/integrations.http.test.ts | 16 +- apps/backend/src/routes/integrations.http.ts | 86 ++++++----- apps/backend/src/routes/internal.http.ts | 3 +- apps/backend/src/routes/klipy.http.ts | 25 ++- apps/backend/src/rpc/handlers/channels.ts | 2 +- .../src/rpc/handlers/connect-shares.test.ts | 38 ++--- .../src/rpc/handlers/connect-shares.ts | 4 +- apps/backend/src/rpc/middleware/auth.ts | 9 +- .../src/rpc/middleware/scope-injection.ts | 2 +- .../chat-sync/chat-sync-core-worker.ts | 40 ++--- .../chat-sync/chat-sync-provider-registry.ts | 2 +- .../chat-sync/discord-gateway-service.ts | 2 +- .../chat-sync/discord-sync-worker.test.ts | 6 +- .../services/connect-conversation-service.ts | 4 +- .../src/services/message-outbox-dispatcher.ts | 16 +- .../services/message-side-effect-service.ts | 2 +- .../src/services/oauth/oauth-http-client.ts | 4 +- .../backend/src/services/org-resolver.test.ts | 44 +++--- .../src/test/message-outbox-repo.test.ts | 2 +- apps/web/src/atoms/emoji-atoms.ts | 7 +- apps/web/src/atoms/feature-discovery-atoms.ts | 7 +- apps/web/src/atoms/hotkey-atoms.ts | 7 +- .../web/src/atoms/notification-sound-atoms.ts | 2 +- apps/web/src/atoms/panel-atoms.ts | 2 +- apps/web/src/atoms/react-scan-atoms.ts | 2 +- apps/web/src/atoms/recent-channels-atom.ts | 2 +- apps/web/src/atoms/search-atoms.ts | 2 +- apps/web/src/atoms/section-collapse-atoms.ts | 7 +- apps/web/src/atoms/sidebar-atoms.ts | 2 +- .../add-github-repo-modal.tsx | 6 +- .../chat-sync/add-channel-link-modal.tsx | 2 +- .../chat-sync/add-connection-modal.tsx | 2 +- .../components/chat/channel-join-banner.tsx | 4 +- .../components/chat/message-reply-section.tsx | 2 +- .../slate-editor/slate-message-editor.tsx | 5 +- .../src/components/gif-picker/use-klipy.ts | 14 +- .../add-github-subscription-modal.tsx | 6 +- .../integrations/github-pr-embed.tsx | 4 +- .../integrations/linear-issue-embed.tsx | 4 +- .../components/modals/join-channel-modal.tsx | 4 +- apps/web/src/components/theme-provider.tsx | 6 +- apps/web/src/db/actions.ts | 36 ++--- apps/web/src/lib/auth-token.ts | 54 +++---- apps/web/src/lib/error-messages.ts | 7 +- .../platform-key-value-store.ts | 2 +- .../platform-storage/tauri-key-value-store.ts | 4 +- .../web/src/lib/services/common/api-client.ts | 5 +- .../src/lib/services/common/network-mode.ts | 1 + apps/web/src/providers/chat-provider.tsx | 22 +-- .../$orgSlug/my-settings/linked-accounts.tsx | 4 +- .../_app/$orgSlug/settings/authentication.tsx | 8 +- .../settings/integrations/$integrationId.tsx | 8 +- apps/web/src/routes/join/$slug.tsx | 2 +- bun.lock | 40 ++--- .../src/collection.ts | 6 +- .../src/handlers.ts | 17 ++- .../src/optimistic-action.ts | 14 +- .../src/service.ts | 6 +- libs/tanstack-db-atom/src/AtomTanStackDB.ts | 60 ++++---- libs/tanstack-db-atom/src/types.ts | 2 +- package.json | 20 +-- packages/domain/src/errors.ts | 55 +++---- 88 files changed, 688 insertions(+), 980 deletions(-) delete mode 100644 apps/backend/fix-static-accessors.mjs delete mode 100644 apps/backend/fix-static-accessors2.mjs diff --git a/apps/backend/fix-static-accessors.mjs b/apps/backend/fix-static-accessors.mjs deleted file mode 100644 index daf5fd4f9..000000000 --- a/apps/backend/fix-static-accessors.mjs +++ /dev/null @@ -1,141 +0,0 @@ -import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs'; -import { join, resolve } from 'path'; - -const serviceMap = { - 'AttachmentPolicy': 'attachmentPolicy', - 'MessagePolicy': 'messagePolicy', - 'ChannelPolicy': 'channelPolicy', - 'ChannelMemberPolicy': 'channelMemberPolicy', - 'ChannelSectionPolicy': 'channelSectionPolicy', - 'OrganizationPolicy': 'organizationPolicy', - 'OrganizationMemberPolicy': 'organizationMemberPolicy', - 'InvitationPolicy': 'invitationPolicy', - 'MessageReactionPolicy': 'messageReactionPolicy', - 'NotificationPolicy': 'notificationPolicy', - 'PinnedMessagePolicy': 'pinnedMessagePolicy', - 'TypingIndicatorPolicy': 'typingIndicatorPolicy', - 'UserPolicy': 'userPolicy', - 'UserPresenceStatusPolicy': 'userPresenceStatusPolicy', - 'IntegrationConnectionPolicy': 'integrationConnectionPolicy', - 'ChannelWebhookPolicy': 'channelWebhookPolicy', - 'GitHubSubscriptionPolicy': 'gitHubSubscriptionPolicy', - 'RssSubscriptionPolicy': 'rssSubscriptionPolicy', - 'BotPolicy': 'botPolicy', - 'CustomEmojiPolicy': 'customEmojiPolicy', - 'AttachmentRepo': 'attachmentRepo', - 'MessageRepo': 'messageRepo', - 'ChannelRepo': 'channelRepo', - 'ChannelMemberRepo': 'channelMemberRepo', - 'ChannelSectionRepo': 'channelSectionRepo', - 'OrganizationRepo': 'organizationRepo', - 'OrganizationMemberRepo': 'organizationMemberRepo', - 'InvitationRepo': 'invitationRepo', - 'UserRepo': 'userRepo', - 'PinnedMessageRepo': 'pinnedMessageRepo', - 'TypingIndicatorRepo': 'typingIndicatorRepo', - 'NotificationRepo': 'notificationRepo', - 'MessageReactionRepo': 'messageReactionRepo', - 'MessageOutboxRepo': 'messageOutboxRepo', - 'UserPresenceStatusRepo': 'userPresenceStatusRepo', - 'IntegrationConnectionRepo': 'integrationConnectionRepo', - 'IntegrationTokenRepo': 'integrationTokenRepo', - 'ChannelWebhookRepo': 'channelWebhookRepo', - 'GitHubSubscriptionRepo': 'gitHubSubscriptionRepo', - 'RssSubscriptionRepo': 'rssSubscriptionRepo', - 'BotRepo': 'botRepo', - 'BotCommandRepo': 'botCommandRepo', - 'BotInstallationRepo': 'botInstallationRepo', - 'CustomEmojiRepo': 'customEmojiRepo', - 'ConnectConversationRepo': 'connectConversationRepo', - 'ConnectConversationChannelRepo': 'connectConversationChannelRepo', - 'ConnectInviteRepo': 'connectInviteRepo', - 'ConnectParticipantRepo': 'connectParticipantRepo', - 'ChatSyncConnectionRepo': 'chatSyncConnectionRepo', - 'ChatSyncChannelLinkRepo': 'chatSyncChannelLinkRepo', - 'ChatSyncMessageLinkRepo': 'chatSyncMessageLinkRepo', - 'ChatSyncEventReceiptRepo': 'chatSyncEventReceiptRepo', - 'ChannelAccessSyncService': 'channelAccessSync', - 'DiscordSyncWorker': 'discordSyncWorker', - 'ConnectConversationService': 'connectConversationService', -}; - -// Skip these patterns (they're not static accessor calls) -const skipPatterns = ['layer', 'toLayer', 'Default', 'make', 'of(']; - -function processFile(filePath) { - let content = readFileSync(filePath, 'utf8'); - const originalContent = content; - - // Find all static accessor usages - const usedServices = new Set(); - for (const [className, varName] of Object.entries(serviceMap)) { - // Match ClassName.something where something starts with lowercase - // and is not a known non-accessor pattern - const regex = new RegExp(`(?( @@ -34,10 +34,14 @@ export const generateTransactionId = Effect.fn("generateTransactionId")(function Effect.tap((decodedTxid) => Effect.log(`[txid-debug] Decoded transactionId: ${decodedTxid}, type: ${typeof decodedTxid}`), ), - Effect.catchTags({ - DatabaseError: (err) => Effect.die(`Database error generating transaction ID: ${err}`), - ParseError: (err) => Effect.die(`Failed to parse transaction ID: ${err}`), - }), + Effect.catchIf( + (e): e is Database.DatabaseError => Predicate.isTagged(e, "DatabaseError"), + (err) => Effect.die(`Database error generating transaction ID: ${err}`), + ), + Effect.catchIf( + SchemaIssue.isIssue, + (err) => Effect.die(`Failed to parse transaction ID: ${err}`), + ), ) return result diff --git a/apps/backend/src/lib/policy-utils.test.ts b/apps/backend/src/lib/policy-utils.test.ts index 6b3497bb8..0cc568df2 100644 --- a/apps/backend/src/lib/policy-utils.test.ts +++ b/apps/backend/src/lib/policy-utils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@effect/vitest" import { UnauthorizedError } from "@hazel/domain" -import { Effect, Either } from "effect" +import { Effect, Result } from "effect" import { makePolicy, withPolicyUnauthorized } from "./policy-utils.ts" import { makeActor } from "../policies/policy-test-helpers.ts" import { CurrentUser } from "@hazel/domain" @@ -12,11 +12,11 @@ describe("policy-utils", () => { const result = await Effect.runPromise( authorize("read", () => Effect.succeed(true)).pipe( Effect.provideService(CurrentUser.Context, makeActor()), - Effect.either, + Effect.result, ), ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("fails with UnauthorizedError when check denies", async () => { @@ -24,12 +24,12 @@ describe("policy-utils", () => { const result = await Effect.runPromise( authorize("read", () => Effect.succeed(false)).pipe( Effect.provideService(CurrentUser.Context, makeActor()), - Effect.either, + Effect.result, ), ) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.left)).toBe(true) } }) @@ -39,12 +39,12 @@ describe("policy-utils", () => { const result = await Effect.runPromise( authorize("read", () => Effect.fail({ _tag: "DatabaseError" as const })).pipe( Effect.provideService(CurrentUser.Context, makeActor()), - Effect.either, + Effect.result, ), ) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.left)).toBe(true) } }) @@ -60,12 +60,12 @@ describe("policy-utils", () => { const result = await Effect.runPromise( withPolicyUnauthorized("Widget", "read", Effect.fail(existing)).pipe( Effect.provideService(CurrentUser.Context, makeActor()), - Effect.either, + Effect.result, ), ) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(result.left).toBe(existing) } }) diff --git a/apps/backend/src/policies/attachment-policy.test.ts b/apps/backend/src/policies/attachment-policy.test.ts index f2c938a9d..8f96aec2b 100644 --- a/apps/backend/src/policies/attachment-policy.test.ts +++ b/apps/backend/src/policies/attachment-policy.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "@effect/vitest" import { AttachmentRepo, ChannelMemberRepo, ChannelRepo, MessageRepo } from "@hazel/backend-core" import type { AttachmentId, ChannelId, MessageId, OrganizationId, UserId } from "@hazel/schema" -import { Effect, Either, Layer, Option } from "effect" +import { Effect, Result, Layer, Option } from "effect" import { AttachmentPolicy } from "./attachment-policy.ts" import { makeActor, @@ -102,7 +102,7 @@ describe("AttachmentPolicy", () => { const layer = makePolicyLayer({}) const result = await runWithActorEither(AttachmentPolicy.canCreate(), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate allows uploader", async () => { @@ -114,7 +114,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canUpdate(ATTACHMENT_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate denies non-uploader", async () => { @@ -126,7 +126,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canUpdate(ATTACHMENT_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canDelete without messageId allows uploader", async () => { @@ -138,7 +138,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canDelete(ATTACHMENT_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete without messageId denies other user", async () => { @@ -150,7 +150,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canDelete(ATTACHMENT_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canDelete with messageId allows uploader", async () => { @@ -168,7 +168,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canDelete(ATTACHMENT_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete with messageId allows message author", async () => { @@ -186,7 +186,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canDelete(ATTACHMENT_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete with messageId allows org admin", async () => { @@ -207,7 +207,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canDelete(ATTACHMENT_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete with messageId denies random user", async () => { @@ -228,7 +228,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canDelete(ATTACHMENT_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canView without messageId allows uploader", async () => { @@ -240,7 +240,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canView(ATTACHMENT_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canView without messageId denies other user", async () => { @@ -252,7 +252,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canView(ATTACHMENT_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canView with public channel allows org member", async () => { @@ -273,7 +273,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canView(ATTACHMENT_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canView with private channel allows admin", async () => { @@ -294,7 +294,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canView(ATTACHMENT_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canView with private channel allows channel member", async () => { @@ -315,7 +315,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canView(ATTACHMENT_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canView with private channel denies non-member non-admin", async () => { @@ -336,6 +336,6 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither(AttachmentPolicy.canView(ATTACHMENT_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) }) diff --git a/apps/backend/src/policies/bot-policy.test.ts b/apps/backend/src/policies/bot-policy.test.ts index db7e4c55d..1775ed29a 100644 --- a/apps/backend/src/policies/bot-policy.test.ts +++ b/apps/backend/src/policies/bot-policy.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import { BotRepo } from "@hazel/backend-core" import { UnauthorizedError } from "@hazel/domain" import type { BotId, UserId } from "@hazel/schema" -import { Effect, Either, Layer } from "effect" +import { Effect, Result, Layer } from "effect" import { BotPolicy } from "./bot-policy.ts" import { makeActor, @@ -48,8 +48,8 @@ describe("BotPolicy", () => { const allowed = await runWithActorEither(BotPolicy.canCreate(TEST_ORG_ID), layer, actor) const denied = await runWithActorEither(BotPolicy.canCreate(TEST_ALT_ORG_ID), layer, actor) - expect(Either.isRight(allowed)).toBe(true) - expect(Either.isLeft(denied)).toBe(true) + expect(Result.isSuccess(allowed)).toBe(true) + expect(Result.isFailure(denied)).toBe(true) }) it("canRead allows creator or org admin", async () => { @@ -80,9 +80,9 @@ describe("BotPolicy", () => { ) const outsiderDenied = await runWithActorEither(BotPolicy.canRead(BOT_ID), layer, outsider) - expect(Either.isRight(creatorAllowed)).toBe(true) - expect(Either.isRight(adminAllowed)).toBe(true) - expect(Either.isLeft(outsiderDenied)).toBe(true) + expect(Result.isSuccess(creatorAllowed)).toBe(true) + expect(Result.isSuccess(adminAllowed)).toBe(true) + expect(Result.isFailure(outsiderDenied)).toBe(true) }) it("canUpdate/canDelete require creator and map missing bot to UnauthorizedError", async () => { @@ -96,10 +96,10 @@ describe("BotPolicy", () => { const updateOther = await runWithActorEither(BotPolicy.canUpdate(BOT_ID), layer, otherUser) const deleteMissing = await runWithActorEither(BotPolicy.canDelete(MISSING_BOT_ID), layer, creator) - expect(Either.isRight(updateCreator)).toBe(true) - expect(Either.isLeft(updateOther)).toBe(true) - expect(Either.isLeft(deleteMissing)).toBe(true) - if (Either.isLeft(deleteMissing)) { + expect(Result.isSuccess(updateCreator)).toBe(true) + expect(Result.isFailure(updateOther)).toBe(true) + expect(Result.isFailure(deleteMissing)).toBe(true) + if (Result.isFailure(deleteMissing)) { expect(UnauthorizedError.is(deleteMissing.left)).toBe(true) } }) @@ -121,8 +121,8 @@ describe("BotPolicy", () => { const uninstallAdmin = await runWithActorEither(BotPolicy.canUninstall(TEST_ORG_ID), layer, admin) const installMember = await runWithActorEither(BotPolicy.canInstall(TEST_ORG_ID), layer, member) - expect(Either.isRight(installAdmin)).toBe(true) - expect(Either.isRight(uninstallAdmin)).toBe(true) - expect(Either.isLeft(installMember)).toBe(true) + expect(Result.isSuccess(installAdmin)).toBe(true) + expect(Result.isSuccess(uninstallAdmin)).toBe(true) + expect(Result.isFailure(installMember)).toBe(true) }) }) diff --git a/apps/backend/src/policies/channel-member-policy.test.ts b/apps/backend/src/policies/channel-member-policy.test.ts index 4a82cf808..52be7cbaf 100644 --- a/apps/backend/src/policies/channel-member-policy.test.ts +++ b/apps/backend/src/policies/channel-member-policy.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import { ChannelMemberRepo, ChannelRepo } from "@hazel/backend-core" import { UnauthorizedError } from "@hazel/domain" import type { ChannelId, ChannelMemberId, OrganizationId, UserId } from "@hazel/schema" -import { Effect, Either, Layer, Option } from "effect" +import { Effect, Result, Layer, Option } from "effect" import { ChannelMemberPolicy } from "./channel-member-policy.ts" import { makeActor, @@ -89,7 +89,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither(ChannelMemberPolicy.isOwner(CHANNEL_MEMBER_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("isOwner denies different user", async () => { @@ -103,8 +103,8 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither(ChannelMemberPolicy.isOwner(CHANNEL_MEMBER_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.left)).toBe(true) } }) @@ -126,8 +126,8 @@ describe("ChannelMemberPolicy", () => { const adminResult = await runWithActorEither(ChannelMemberPolicy.canCreate(CHANNEL_ID), layer, admin) const ownerResult = await runWithActorEither(ChannelMemberPolicy.canCreate(CHANNEL_ID), layer, owner) - expect(Either.isRight(adminResult)).toBe(true) - expect(Either.isRight(ownerResult)).toBe(true) + expect(Result.isSuccess(adminResult)).toBe(true) + expect(Result.isSuccess(ownerResult)).toBe(true) }) it("canCreate allows member for public channel", async () => { @@ -142,7 +142,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither(ChannelMemberPolicy.canCreate(CHANNEL_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canCreate denies member for private channel", async () => { @@ -157,8 +157,8 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither(ChannelMemberPolicy.canCreate(CHANNEL_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.left)).toBe(true) } }) @@ -178,7 +178,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither(ChannelMemberPolicy.canRead(CHANNEL_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canRead allows org admin even without channel membership", async () => { @@ -194,7 +194,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither(ChannelMemberPolicy.canRead(CHANNEL_ID), layer, admin) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canRead denies non-admin non-channel-member", async () => { @@ -210,8 +210,8 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither(ChannelMemberPolicy.canRead(CHANNEL_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.left)).toBe(true) } }) @@ -235,7 +235,7 @@ describe("ChannelMemberPolicy", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate allows org admin but NOT org owner", async () => { @@ -268,10 +268,10 @@ describe("ChannelMemberPolicy", () => { ) // Admin is allowed - expect(Either.isRight(adminResult)).toBe(true) + expect(Result.isSuccess(adminResult)).toBe(true) // Owner is NOT allowed (canUpdate checks role === "admin", not isAdminOrOwner) - expect(Either.isLeft(ownerResult)).toBe(true) - if (Either.isLeft(ownerResult)) { + expect(Result.isFailure(ownerResult)).toBe(true) + if (Result.isFailure(ownerResult)) { expect(UnauthorizedError.is(ownerResult.left)).toBe(true) } }) @@ -295,7 +295,7 @@ describe("ChannelMemberPolicy", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete allows org admin but NOT org owner", async () => { @@ -328,10 +328,10 @@ describe("ChannelMemberPolicy", () => { ) // Admin is allowed - expect(Either.isRight(adminResult)).toBe(true) + expect(Result.isSuccess(adminResult)).toBe(true) // Owner is NOT allowed (canDelete checks role === "admin", not isAdminOrOwner) - expect(Either.isLeft(ownerResult)).toBe(true) - if (Either.isLeft(ownerResult)) { + expect(Result.isFailure(ownerResult)).toBe(true) + if (Result.isFailure(ownerResult)) { expect(UnauthorizedError.is(ownerResult.left)).toBe(true) } }) diff --git a/apps/backend/src/policies/channel-policy.test.ts b/apps/backend/src/policies/channel-policy.test.ts index af362db00..d59c6e9a9 100644 --- a/apps/backend/src/policies/channel-policy.test.ts +++ b/apps/backend/src/policies/channel-policy.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import { ChannelRepo } from "@hazel/backend-core" import { UnauthorizedError } from "@hazel/domain" import type { ChannelId, OrganizationId } from "@hazel/schema" -import { Effect, Either, Layer } from "effect" +import { Effect, Result, Layer } from "effect" import { ChannelPolicy } from "./channel-policy.ts" import { makeActor, @@ -74,10 +74,10 @@ describe("ChannelPolicy", () => { ["channels:write"], ) - expect(Either.isLeft(memberResult)).toBe(true) - expect(Either.isRight(adminResult)).toBe(true) - expect(Either.isRight(ownerResult)).toBe(true) - expect(Either.isLeft(noMembership)).toBe(true) + expect(Result.isFailure(memberResult)).toBe(true) + expect(Result.isSuccess(adminResult)).toBe(true) + expect(Result.isSuccess(ownerResult)).toBe(true) + expect(Result.isFailure(noMembership)).toBe(true) }) it("canUpdate allows org admins and maps not-found to UnauthorizedError", async () => { @@ -94,9 +94,9 @@ describe("ChannelPolicy", () => { const allowed = await runWithActorEither(ChannelPolicy.canUpdate(CHANNEL_ID), layer, actor) const missing = await runWithActorEither(ChannelPolicy.canUpdate(MISSING_CHANNEL_ID), layer, actor) - expect(Either.isRight(allowed)).toBe(true) - expect(Either.isLeft(missing)).toBe(true) - if (Either.isLeft(missing)) { + expect(Result.isSuccess(allowed)).toBe(true) + expect(Result.isFailure(missing)).toBe(true) + if (Result.isFailure(missing)) { expect(UnauthorizedError.is(missing.left)).toBe(true) } }) @@ -113,6 +113,6 @@ describe("ChannelPolicy", () => { ) const result = await runWithActorEither(ChannelPolicy.canDelete(CHANNEL_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) }) diff --git a/apps/backend/src/policies/integration-connection-policy.test.ts b/apps/backend/src/policies/integration-connection-policy.test.ts index a37810403..1394b823d 100644 --- a/apps/backend/src/policies/integration-connection-policy.test.ts +++ b/apps/backend/src/policies/integration-connection-policy.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@effect/vitest" import { UnauthorizedError } from "@hazel/domain" -import { Either, Layer } from "effect" +import { Result, Layer } from "effect" import { IntegrationConnectionPolicy } from "./integration-connection-policy.ts" import { makeActor, makeOrgResolverLayer, runWithActorEither, TEST_ORG_ID } from "./policy-test-helpers.ts" @@ -21,7 +21,7 @@ describe("IntegrationConnectionPolicy", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("allows insert/update/delete for admin-or-owner only", async () => { @@ -42,11 +42,11 @@ describe("IntegrationConnectionPolicy", () => { ) const del = await runWithActorEither(IntegrationConnectionPolicy.canDelete(TEST_ORG_ID), layer, actor) - expect(Either.isLeft(insert)).toBe(true) - expect(Either.isLeft(update)).toBe(true) - expect(Either.isLeft(del)).toBe(true) + expect(Result.isFailure(insert)).toBe(true) + expect(Result.isFailure(update)).toBe(true) + expect(Result.isFailure(del)).toBe(true) - if (Either.isLeft(insert)) { + if (Result.isFailure(insert)) { expect(UnauthorizedError.is(insert.left)).toBe(true) } }) diff --git a/apps/backend/src/policies/invitation-policy.test.ts b/apps/backend/src/policies/invitation-policy.test.ts index bd1d0c2bd..07c8f7812 100644 --- a/apps/backend/src/policies/invitation-policy.test.ts +++ b/apps/backend/src/policies/invitation-policy.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import { InvitationRepo, UserRepo } from "@hazel/backend-core" import { UnauthorizedError } from "@hazel/domain" import type { InvitationId, OrganizationId, UserId } from "@hazel/schema" -import { Effect, Either, Layer, Option } from "effect" +import { Effect, Result, Layer, Option } from "effect" import { InvitationPolicy } from "./invitation-policy.ts" import { makeActor, @@ -67,7 +67,7 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canRead(INVITATION_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canCreate allows admin-or-owner", async () => { @@ -81,7 +81,7 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canCreate(TEST_ORG_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canCreate denies regular member", async () => { @@ -95,7 +95,7 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canCreate(TEST_ORG_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canUpdate allows creator", async () => { @@ -113,7 +113,7 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canUpdate(INVITATION_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate allows org admin who is not creator", async () => { @@ -133,7 +133,7 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canUpdate(INVITATION_ID), layer, admin) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate denies non-creator non-admin", async () => { @@ -153,7 +153,7 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canUpdate(INVITATION_ID), layer, outsider) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canDelete allows creator", async () => { @@ -171,7 +171,7 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canDelete(INVITATION_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete allows org admin who is not creator", async () => { @@ -191,7 +191,7 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canDelete(INVITATION_ID), layer, admin) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canAccept allows when user email matches invitation email", async () => { @@ -212,7 +212,7 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canAccept(INVITATION_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canAccept denies when user email does not match", async () => { @@ -233,7 +233,7 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canAccept(INVITATION_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canAccept denies when user is not found", async () => { @@ -252,7 +252,7 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canAccept(INVITATION_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canList allows admin-or-owner", async () => { @@ -266,7 +266,7 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canList(TEST_ORG_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canList denies regular member", async () => { @@ -280,6 +280,6 @@ describe("InvitationPolicy", () => { const result = await runWithActorEither(InvitationPolicy.canList(TEST_ORG_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) }) diff --git a/apps/backend/src/policies/message-policy.test.ts b/apps/backend/src/policies/message-policy.test.ts index 0eb188cce..ff04387bc 100644 --- a/apps/backend/src/policies/message-policy.test.ts +++ b/apps/backend/src/policies/message-policy.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import { ChannelMemberRepo, ChannelRepo, MessageRepo, OrganizationMemberRepo } from "@hazel/backend-core" import { UnauthorizedError } from "@hazel/domain" import type { ChannelId, MessageId, OrganizationId, UserId } from "@hazel/schema" -import { Effect, Either, Layer, Option } from "effect" +import { Effect, Result, Layer, Option } from "effect" import { OrgResolver } from "../services/org-resolver.ts" import { MessagePolicy } from "./message-policy.ts" import { @@ -114,7 +114,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither(MessagePolicy.canCreate(CHANNEL_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canCreate denies non-org-member", async () => { @@ -130,7 +130,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither(MessagePolicy.canCreate(CHANNEL_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canRead allows org member with channel access", async () => { @@ -146,7 +146,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither(MessagePolicy.canRead(CHANNEL_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate allows message author", async () => { @@ -164,7 +164,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither(MessagePolicy.canUpdate(MESSAGE_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate denies non-author", async () => { @@ -184,7 +184,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither(MessagePolicy.canUpdate(MESSAGE_ID), layer, otherUser) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canDelete allows message author", async () => { @@ -202,7 +202,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither(MessagePolicy.canDelete(MESSAGE_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete allows org admin who is not author", async () => { @@ -222,7 +222,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither(MessagePolicy.canDelete(MESSAGE_ID), layer, admin) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete denies org member who is not author and not admin", async () => { @@ -242,7 +242,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither(MessagePolicy.canDelete(MESSAGE_ID), layer, member) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canDelete maps missing message to UnauthorizedError", async () => { @@ -258,8 +258,8 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither(MessagePolicy.canDelete(MISSING_MESSAGE_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.left)).toBe(true) } }) diff --git a/apps/backend/src/policies/message-reaction-policy.test.ts b/apps/backend/src/policies/message-reaction-policy.test.ts index 4d7fa7e3f..b84ec7f2a 100644 --- a/apps/backend/src/policies/message-reaction-policy.test.ts +++ b/apps/backend/src/policies/message-reaction-policy.test.ts @@ -15,7 +15,7 @@ import type { OrganizationId, UserId, } from "@hazel/schema" -import { Effect, Either, Layer, Option } from "effect" +import { Effect, Result, Layer, Option } from "effect" import { MessageReactionPolicy } from "./message-reaction-policy.ts" import { ConnectConversationService } from "../services/connect-conversation-service.ts" import { OrgResolver } from "../services/org-resolver.ts" @@ -114,7 +114,7 @@ describe("MessageReactionPolicy", () => { const layer = makePolicyLayer({}, {}, {}, {}) const result = await runWithActorEither(MessageReactionPolicy.canList(MESSAGE_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canCreate allows org member with channel access", async () => { @@ -127,7 +127,7 @@ describe("MessageReactionPolicy", () => { ) const result = await runWithActorEither(MessageReactionPolicy.canCreate(MESSAGE_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canCreate denies non-org-member", async () => { @@ -140,7 +140,7 @@ describe("MessageReactionPolicy", () => { ) const result = await runWithActorEither(MessageReactionPolicy.canCreate(MESSAGE_ID), layer, outsider) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canUpdate allows reaction owner", async () => { @@ -148,7 +148,7 @@ describe("MessageReactionPolicy", () => { const layer = makePolicyLayer({}, { [REACTION_ID]: { userId: actor.id } }, {}, {}) const result = await runWithActorEither(MessageReactionPolicy.canUpdate(REACTION_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate denies non-owner", async () => { @@ -156,7 +156,7 @@ describe("MessageReactionPolicy", () => { const layer = makePolicyLayer({}, { [REACTION_ID]: { userId: OTHER_USER_ID } }, {}, {}) const result = await runWithActorEither(MessageReactionPolicy.canUpdate(REACTION_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canDelete allows reaction owner", async () => { @@ -164,7 +164,7 @@ describe("MessageReactionPolicy", () => { const layer = makePolicyLayer({}, { [REACTION_ID]: { userId: actor.id } }, {}, {}) const result = await runWithActorEither(MessageReactionPolicy.canDelete(REACTION_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete denies non-owner", async () => { @@ -172,8 +172,8 @@ describe("MessageReactionPolicy", () => { const layer = makePolicyLayer({}, { [REACTION_ID]: { userId: OTHER_USER_ID } }, {}, {}) const result = await runWithActorEither(MessageReactionPolicy.canDelete(REACTION_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.left)).toBe(true) } }) diff --git a/apps/backend/src/policies/notification-policy.test.ts b/apps/backend/src/policies/notification-policy.test.ts index 366bb26a0..859a7a057 100644 --- a/apps/backend/src/policies/notification-policy.test.ts +++ b/apps/backend/src/policies/notification-policy.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import { NotificationRepo, OrganizationMemberRepo } from "@hazel/backend-core" import { UnauthorizedError } from "@hazel/domain" import type { NotificationId, OrganizationId, OrganizationMemberId, UserId } from "@hazel/schema" -import { Effect, Either, Layer, Option } from "effect" +import { Effect, Result, Layer, Option } from "effect" import { NotificationPolicy } from "./notification-policy.ts" import { makeActor, @@ -68,7 +68,7 @@ describe("NotificationPolicy", () => { const layer = makePolicyLayer({}, {}, {}) const result = await runWithActorEither(NotificationPolicy.canCreate(MEMBER_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canView allows notification owner", async () => { @@ -80,7 +80,7 @@ describe("NotificationPolicy", () => { ) const result = await runWithActorEither(NotificationPolicy.canView(NOTIFICATION_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canView denies other user", async () => { @@ -92,7 +92,7 @@ describe("NotificationPolicy", () => { ) const result = await runWithActorEither(NotificationPolicy.canView(NOTIFICATION_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canUpdate allows notification owner", async () => { @@ -104,7 +104,7 @@ describe("NotificationPolicy", () => { ) const result = await runWithActorEither(NotificationPolicy.canUpdate(NOTIFICATION_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate allows org admin", async () => { @@ -116,7 +116,7 @@ describe("NotificationPolicy", () => { ) const result = await runWithActorEither(NotificationPolicy.canUpdate(NOTIFICATION_ID), layer, admin) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate denies non-owner non-admin", async () => { @@ -138,7 +138,7 @@ describe("NotificationPolicy", () => { layer, outsider, ) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canDelete allows notification owner", async () => { @@ -150,7 +150,7 @@ describe("NotificationPolicy", () => { ) const result = await runWithActorEither(NotificationPolicy.canDelete(NOTIFICATION_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete allows org admin", async () => { @@ -162,7 +162,7 @@ describe("NotificationPolicy", () => { ) const result = await runWithActorEither(NotificationPolicy.canDelete(NOTIFICATION_ID), layer, admin) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canMarkAsRead allows notification owner", async () => { @@ -178,7 +178,7 @@ describe("NotificationPolicy", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canMarkAllAsRead allows member owner", async () => { @@ -190,7 +190,7 @@ describe("NotificationPolicy", () => { ) const result = await runWithActorEither(NotificationPolicy.canMarkAllAsRead(MEMBER_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canMarkAllAsRead allows org admin", async () => { @@ -202,7 +202,7 @@ describe("NotificationPolicy", () => { ) const result = await runWithActorEither(NotificationPolicy.canMarkAllAsRead(MEMBER_ID), layer, admin) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canMarkAllAsRead denies outsider", async () => { @@ -224,6 +224,6 @@ describe("NotificationPolicy", () => { layer, outsider, ) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) }) diff --git a/apps/backend/src/policies/organization-member-policy.test.ts b/apps/backend/src/policies/organization-member-policy.test.ts index b7f0a51c1..0f483be0e 100644 --- a/apps/backend/src/policies/organization-member-policy.test.ts +++ b/apps/backend/src/policies/organization-member-policy.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import { OrganizationMemberRepo } from "@hazel/backend-core" import { UnauthorizedError } from "@hazel/domain" import type { OrganizationId, OrganizationMemberId, UserId } from "@hazel/schema" -import { Effect, Either, Layer, Option } from "effect" +import { Effect, Result, Layer, Option } from "effect" import { OrganizationMemberPolicy } from "./organization-member-policy.ts" import { makeActor, @@ -46,7 +46,7 @@ describe("OrganizationMemberPolicy", () => { const layer = makePolicyLayer({}, {}) const result = await runWithActorEither(OrganizationMemberPolicy.canCreate(TEST_ORG_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canCreate denies already-existing member", async () => { @@ -54,7 +54,7 @@ describe("OrganizationMemberPolicy", () => { const layer = makePolicyLayer({}, { [`${TEST_ORG_ID}:${actor.id}`]: "member" }) const result = await runWithActorEither(OrganizationMemberPolicy.canCreate(TEST_ORG_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canUpdate allows self-update", async () => { @@ -65,7 +65,7 @@ describe("OrganizationMemberPolicy", () => { ) const result = await runWithActorEither(OrganizationMemberPolicy.canUpdate(MEMBER_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate allows org admin", async () => { @@ -76,7 +76,7 @@ describe("OrganizationMemberPolicy", () => { ) const result = await runWithActorEither(OrganizationMemberPolicy.canUpdate(MEMBER_ID), layer, admin) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate denies org owner (only admin allowed)", async () => { @@ -87,8 +87,8 @@ describe("OrganizationMemberPolicy", () => { ) const result = await runWithActorEither(OrganizationMemberPolicy.canUpdate(MEMBER_ID), layer, owner) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.left)).toBe(true) } }) @@ -105,7 +105,7 @@ describe("OrganizationMemberPolicy", () => { layer, outsider, ) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canDelete allows self-removal", async () => { @@ -116,7 +116,7 @@ describe("OrganizationMemberPolicy", () => { ) const result = await runWithActorEither(OrganizationMemberPolicy.canDelete(MEMBER_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete allows org admin", async () => { @@ -127,7 +127,7 @@ describe("OrganizationMemberPolicy", () => { ) const result = await runWithActorEither(OrganizationMemberPolicy.canDelete(MEMBER_ID), layer, admin) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete denies org owner (only admin allowed)", async () => { @@ -138,8 +138,8 @@ describe("OrganizationMemberPolicy", () => { ) const result = await runWithActorEither(OrganizationMemberPolicy.canDelete(MEMBER_ID), layer, owner) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.left)).toBe(true) } }) diff --git a/apps/backend/src/policies/organization-policy.test.ts b/apps/backend/src/policies/organization-policy.test.ts index a2ad21588..77b29797f 100644 --- a/apps/backend/src/policies/organization-policy.test.ts +++ b/apps/backend/src/policies/organization-policy.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "@effect/vitest" import { UnauthorizedError } from "@hazel/domain" import type { UserId } from "@hazel/schema" -import { Either, Layer } from "effect" +import { Result, Layer } from "effect" import { OrganizationPolicy } from "./organization-policy.ts" import { makeActor, @@ -23,7 +23,7 @@ const makePolicyLayer = (members: Record) => describe("OrganizationPolicy", () => { it("canCreate allows any authenticated actor", async () => { const result = await runWithActorEither(OrganizationPolicy.canCreate(), makePolicyLayer({})) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate allows admin and owner, denies plain member", async () => { @@ -48,9 +48,9 @@ describe("OrganizationPolicy", () => { memberActor, ) - expect(Either.isRight(adminResult)).toBe(true) - expect(Either.isLeft(memberResult)).toBe(true) - if (Either.isLeft(memberResult)) { + expect(Result.isSuccess(adminResult)).toBe(true) + expect(Result.isFailure(memberResult)).toBe(true) + if (Result.isFailure(memberResult)) { expect(UnauthorizedError.is(memberResult.left)).toBe(true) } }) @@ -77,8 +77,8 @@ describe("OrganizationPolicy", () => { adminActor, ) - expect(Either.isRight(ownerResult)).toBe(true) - expect(Either.isLeft(adminResult)).toBe(true) + expect(Result.isSuccess(ownerResult)).toBe(true) + expect(Result.isFailure(adminResult)).toBe(true) }) it("isMember denies users without membership in target org", async () => { @@ -88,8 +88,8 @@ describe("OrganizationPolicy", () => { }) const result = await runWithActorEither(OrganizationPolicy.isMember(TEST_ORG_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.left)).toBe(true) } }) @@ -105,6 +105,6 @@ describe("OrganizationPolicy", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) }) diff --git a/apps/backend/src/policies/pinned-message-policy.test.ts b/apps/backend/src/policies/pinned-message-policy.test.ts index 7a3d8f335..f2b75c981 100644 --- a/apps/backend/src/policies/pinned-message-policy.test.ts +++ b/apps/backend/src/policies/pinned-message-policy.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import { ChannelRepo, OrganizationMemberRepo, PinnedMessageRepo } from "@hazel/backend-core" import { UnauthorizedError } from "@hazel/domain" import type { ChannelId, OrganizationId, PinnedMessageId, UserId } from "@hazel/schema" -import { Effect, Either, Layer, Option } from "effect" +import { Effect, Result, Layer, Option } from "effect" import { PinnedMessagePolicy } from "./pinned-message-policy.ts" import { makeActor, @@ -70,7 +70,7 @@ describe("PinnedMessagePolicy", () => { ) const result = await runWithActorEither(PinnedMessagePolicy.canCreate(CHANNEL_ID), layer, admin) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canCreate allows member in public channel", async () => { @@ -82,7 +82,7 @@ describe("PinnedMessagePolicy", () => { ) const result = await runWithActorEither(PinnedMessagePolicy.canCreate(CHANNEL_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canCreate denies member in private channel", async () => { @@ -94,7 +94,7 @@ describe("PinnedMessagePolicy", () => { ) const result = await runWithActorEither(PinnedMessagePolicy.canCreate(CHANNEL_ID), layer, actor) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canCreate denies non-org-member", async () => { @@ -106,7 +106,7 @@ describe("PinnedMessagePolicy", () => { ) const result = await runWithActorEither(PinnedMessagePolicy.canCreate(CHANNEL_ID), layer, outsider) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canUpdate allows pinner", async () => { @@ -118,7 +118,7 @@ describe("PinnedMessagePolicy", () => { ) const result = await runWithActorEither(PinnedMessagePolicy.canUpdate(PINNED_MSG_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate allows org admin who is not pinner", async () => { @@ -130,7 +130,7 @@ describe("PinnedMessagePolicy", () => { ) const result = await runWithActorEither(PinnedMessagePolicy.canUpdate(PINNED_MSG_ID), layer, admin) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate denies non-pinner non-admin", async () => { @@ -142,7 +142,7 @@ describe("PinnedMessagePolicy", () => { ) const result = await runWithActorEither(PinnedMessagePolicy.canUpdate(PINNED_MSG_ID), layer, outsider) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("canDelete allows pinner", async () => { @@ -154,7 +154,7 @@ describe("PinnedMessagePolicy", () => { ) const result = await runWithActorEither(PinnedMessagePolicy.canDelete(PINNED_MSG_ID), layer, actor) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete allows org admin who is not pinner", async () => { @@ -166,7 +166,7 @@ describe("PinnedMessagePolicy", () => { ) const result = await runWithActorEither(PinnedMessagePolicy.canDelete(PINNED_MSG_ID), layer, admin) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canDelete denies non-pinner non-admin", async () => { @@ -178,8 +178,8 @@ describe("PinnedMessagePolicy", () => { ) const result = await runWithActorEither(PinnedMessagePolicy.canDelete(PINNED_MSG_ID), layer, outsider) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.left)).toBe(true) } }) diff --git a/apps/backend/src/policies/policy-test-helpers.ts b/apps/backend/src/policies/policy-test-helpers.ts index ce61dd964..b134c4278 100644 --- a/apps/backend/src/policies/policy-test-helpers.ts +++ b/apps/backend/src/policies/policy-test-helpers.ts @@ -3,7 +3,7 @@ import { CurrentUser } from "@hazel/domain" import type { ApiScope } from "@hazel/domain/scopes" import { CurrentRpcScopes } from "@hazel/domain/scopes" import type { ChannelId, ChannelMemberId, MessageId, OrganizationId, UserId } from "@hazel/schema" -import { Effect, FiberRef, Layer, Option } from "effect" +import { Effect, Layer, Option } from "effect" import { OrgResolver } from "../services/org-resolver" export const TEST_ORG_ID = "00000000-0000-0000-0000-000000000001" as OrganizationId @@ -31,11 +31,11 @@ export const runWithActorEither = ( scopes: ReadonlyArray = ["messages:read"], ) => Effect.runPromise( - effect.pipe( - Effect.locally(CurrentRpcScopes, scopes), + make.pipe( + Effect.provideService(CurrentRpcScopes, scopes), Effect.provide(layer), Effect.provideService(CurrentUser.Context, actor), - Effect.either, + Effect.result, ), ) diff --git a/apps/backend/src/policies/typing-indicator-policy.test.ts b/apps/backend/src/policies/typing-indicator-policy.test.ts index 7c7532966..049f4696c 100644 --- a/apps/backend/src/policies/typing-indicator-policy.test.ts +++ b/apps/backend/src/policies/typing-indicator-policy.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import { ChannelMemberRepo, TypingIndicatorRepo } from "@hazel/backend-core" import { UnauthorizedError } from "@hazel/domain" import type { ChannelId, ChannelMemberId, TypingIndicatorId, UserId } from "@hazel/schema" -import { Effect, Either, Layer, Option } from "effect" +import { Effect, Result, Layer, Option } from "effect" import { TypingIndicatorPolicy } from "./typing-indicator-policy.ts" import { makeActor, makeEntityNotFound, runWithActorEither, TEST_ORG_ID } from "./policy-test-helpers.ts" @@ -61,7 +61,7 @@ describe("TypingIndicatorPolicy", () => { it("canRead always allows authenticated actors", async () => { const layer = makePolicyLayer({}, {}, {}) const result = await runWithActorEither(TypingIndicatorPolicy.canRead(INDICATOR_ID), layer) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("canCreate requires channel membership", async () => { @@ -86,8 +86,8 @@ describe("TypingIndicatorPolicy", () => { actor, ) - expect(Either.isRight(allowed)).toBe(true) - expect(Either.isLeft(denied)).toBe(true) + expect(Result.isSuccess(allowed)).toBe(true) + expect(Result.isFailure(denied)).toBe(true) }) it("canUpdate allows only the member owner and maps missing indicator to UnauthorizedError", async () => { @@ -131,10 +131,10 @@ describe("TypingIndicatorPolicy", () => { actor, ) - expect(Either.isRight(ownerAllowed)).toBe(true) - expect(Either.isLeft(otherDenied)).toBe(true) - expect(Either.isLeft(missingDenied)).toBe(true) - if (Either.isLeft(missingDenied)) { + expect(Result.isSuccess(ownerAllowed)).toBe(true) + expect(Result.isFailure(otherDenied)).toBe(true) + expect(Result.isFailure(missingDenied)).toBe(true) + if (Result.isFailure(missingDenied)) { expect(UnauthorizedError.is(missingDenied.left)).toBe(true) } }) @@ -182,8 +182,8 @@ describe("TypingIndicatorPolicy", () => { actor, ) - expect(Either.isRight(byMemberAllowed)).toBe(true) - expect(Either.isRight(byIndicatorAllowed)).toBe(true) - expect(Either.isLeft(byMemberDenied)).toBe(true) + expect(Result.isSuccess(byMemberAllowed)).toBe(true) + expect(Result.isSuccess(byIndicatorAllowed)).toBe(true) + expect(Result.isFailure(byMemberDenied)).toBe(true) }) }) diff --git a/apps/backend/src/routes/api-v1/messages.http.ts b/apps/backend/src/routes/api-v1/messages.http.ts index f844b6a87..a3e1159c7 100644 --- a/apps/backend/src/routes/api-v1/messages.http.ts +++ b/apps/backend/src/routes/api-v1/messages.http.ts @@ -44,12 +44,6 @@ async function hashToken(token: string): Promise { */ const authenticateBotFromToken = Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest - const attachmentPolicy = yield* AttachmentPolicy - const messagePolicy = yield* MessagePolicy - const messageReactionPolicy = yield* MessageReactionPolicy - const attachmentRepo = yield* AttachmentRepo - const messageRepo = yield* MessageRepo - const messageReactionRepo = yield* MessageReactionRepo const authHeader = request.headers.authorization if (!authHeader || !authHeader.startsWith("Bearer ")) { @@ -97,25 +91,31 @@ const createBotUserContext = (bot: { userId: typeof import("@hazel/schema").User const withHttpScopes = ( scopes: ReadonlyArray, make: Effect.Effect, -): Effect.Effect => Effect.locally(CurrentRpcScopes, scopes)(effect) +): Effect.Effect => Effect.provideService(make, CurrentRpcScopes, scopes as any) export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messages", (handlers) => Effect.gen(function* () { const db = yield* Database.Database const botGateway = yield* BotGatewayService const outboxRepo = yield* MessageOutboxRepo + const messagePolicy = yield* MessagePolicy + const messageRepo = yield* MessageRepo + const messageReactionPolicy = yield* MessageReactionPolicy + const messageReactionRepo = yield* MessageReactionRepo + const attachmentPolicy = yield* AttachmentPolicy + const attachmentRepo = yield* AttachmentRepo return ( handlers // List Messages (with cursor-based pagination) - .handle("listMessages", ({ urlParams }) => + .handle("listMessages", ({ query }) => withHttpScopes( ["messages:read"], Effect.gen(function* () { const bot = yield* authenticateBotFromToken const currentUser = createBotUserContext(bot) - const { channel_id, starting_after, ending_before, limit } = urlParams + const { channel_id, starting_after, ending_before, limit } = query // Validate: cannot specify both cursors if (starting_after && ending_before) { @@ -298,7 +298,7 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag ) // Update Message - .handle("updateMessage", ({ path, payload }) => + .handle("updateMessage", ({ params, payload }) => withHttpScopes( ["messages:write"], Effect.gen(function* () { @@ -312,9 +312,9 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag const response = yield* db .transaction( Effect.gen(function* () { - yield* messagePolicy.canUpdate(path.id) + yield* messagePolicy.canUpdate(params.id) const updatedMessage = yield* messageRepo.update({ - id: path.id, + id: params.id, ...rest, ...(embeds !== undefined ? { embeds } : {}), }) @@ -365,21 +365,21 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag ) // Delete Message - .handle("deleteMessage", ({ path }) => + .handle("deleteMessage", ({ params }) => withHttpScopes( ["messages:write"], Effect.gen(function* () { const bot = yield* authenticateBotFromToken const currentUser = createBotUserContext(bot) - const existingMessage = yield* messageRepo.findById(path.id) + const existingMessage = yield* messageRepo.findById(params.id) yield* checkMessageRateLimit(bot.userId) const response = yield* db .transaction( Effect.gen(function* () { - yield* messagePolicy.canDelete(path.id) - yield* messageRepo.deleteById(path.id) + yield* messagePolicy.canDelete(params.id) + yield* messageRepo.deleteById(params.id) if (Option.isSome(existingMessage)) { yield* outboxRepo.insert({ @@ -434,7 +434,7 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag ) // Toggle Reaction - .handle("toggleReaction", ({ path, payload }) => + .handle("toggleReaction", ({ params, payload }) => withHttpScopes( ["message-reactions:write"], Effect.gen(function* () { @@ -445,7 +445,7 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag .transaction( Effect.gen(function* () { const { emoji, channelId } = payload - const messageId = path.id + const messageId = params.id yield* messageReactionPolicy.canList(messageId) const existingReaction = diff --git a/apps/backend/src/routes/auth.http.ts b/apps/backend/src/routes/auth.http.ts index 95fe7c281..26496b927 100644 --- a/apps/backend/src/routes/auth.http.ts +++ b/apps/backend/src/routes/auth.http.ts @@ -11,23 +11,23 @@ import { WorkOSAuth as WorkOS } from "../services/workos-auth" export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => handlers - .handle("login", ({ urlParams }) => + .handle("login", ({ query }) => Effect.gen(function* () { const workos = yield* WorkOS - const clientId = yield* Config.string("WORKOS_CLIENT_ID").pipe(Effect.orDie) - const redirectUri = yield* Config.string("WORKOS_REDIRECT_URI").pipe(Effect.orDie) + const clientId = yield* Config.string("WORKOS_CLIENT_ID") + const redirectUri = yield* Config.string("WORKOS_REDIRECT_URI") // Validate returnTo is a relative URL (defense in depth) - const validatedReturnTo = Schema.decodeSync(RelativeUrl)(urlParams.returnTo) + const validatedReturnTo = Schema.decodeSync(RelativeUrl)(query.returnTo) const state = JSON.stringify(AuthState.make({ returnTo: validatedReturnTo })) let workosOrgId: string - if (urlParams.organizationId) { + if (query.organizationId) { const workosOrg = yield* workos .call(async (client) => - client.organizations.getOrganizationByExternalId(urlParams.organizationId!), + client.organizations.getOrganizationByExternalId(query.organizationId!), ) .pipe( Effect.catchTag("WorkOSAuthError", (error) => @@ -55,7 +55,7 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => ...(workosOrgId && { organizationId: workosOrgId, }), - ...(urlParams.invitationToken && { invitationToken: urlParams.invitationToken }), + ...(query.invitationToken && { invitationToken: query.invitationToken }), }) return authUrl }) @@ -81,12 +81,12 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => }) }), ) - .handle("callback", ({ urlParams }) => + .handle("callback", ({ query }) => Effect.gen(function* () { - const frontendUrl = yield* Config.string("FRONTEND_URL").pipe(Effect.orDie) + const frontendUrl = yield* Config.string("FRONTEND_URL") - const code = urlParams.code - const state = urlParams.state + const code = query.code + const state = query.state if (!code) { return yield* Effect.fail( @@ -111,12 +111,12 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => }) }), ) - .handle("logout", ({ urlParams }) => + .handle("logout", ({ query }) => Effect.gen(function* () { - const frontendUrl = yield* Config.string("FRONTEND_URL").pipe(Effect.orDie) + const frontendUrl = yield* Config.string("FRONTEND_URL") // Build the full return URL - redirect to frontend after logout - const returnTo = urlParams.redirectTo ? `${frontendUrl}${urlParams.redirectTo}` : frontendUrl + const returnTo = query.redirectTo ? `${frontendUrl}${query.redirectTo}` : frontendUrl return HttpServerResponse.empty({ status: 302, @@ -126,33 +126,33 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => }) }), ) - .handle("loginDesktop", ({ urlParams }) => + .handle("loginDesktop", ({ query }) => Effect.gen(function* () { const workos = yield* WorkOS - const clientId = yield* Config.string("WORKOS_CLIENT_ID").pipe(Effect.orDie) - const frontendUrl = yield* Config.string("FRONTEND_URL").pipe(Effect.orDie) + const clientId = yield* Config.string("WORKOS_CLIENT_ID") + const frontendUrl = yield* Config.string("FRONTEND_URL") // Always use web app callback page const redirectUri = `${frontendUrl}/auth/desktop-callback` // Validate returnTo is a relative URL (defense in depth) - const validatedReturnTo = Schema.decodeSync(RelativeUrl)(urlParams.returnTo) + const validatedReturnTo = Schema.decodeSync(RelativeUrl)(query.returnTo) // Build state with desktop connection info const stateObj = DesktopAuthState.make({ returnTo: validatedReturnTo, - desktopPort: urlParams.desktopPort, - desktopNonce: urlParams.desktopNonce, + desktopPort: query.desktopPort, + desktopNonce: query.desktopNonce, }) const state = JSON.stringify(stateObj) let workosOrgId: string | undefined - if (urlParams.organizationId) { + if (query.organizationId) { const workosOrg = yield* workos .call(async (client) => - client.organizations.getOrganizationByExternalId(urlParams.organizationId!), + client.organizations.getOrganizationByExternalId(query.organizationId!), ) .pipe(Effect.catchTag("WorkOSAuthError", () => Effect.succeed(null))) @@ -167,7 +167,7 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => redirectUri, state, ...(workosOrgId && { organizationId: workosOrgId }), - ...(urlParams.invitationToken && { invitationToken: urlParams.invitationToken }), + ...(query.invitationToken && { invitationToken: query.invitationToken }), }) }) .pipe( @@ -197,7 +197,7 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => const { code, state } = payload - const clientId = yield* Config.string("WORKOS_CLIENT_ID").pipe(Effect.orDie) + const clientId = yield* Config.string("WORKOS_CLIENT_ID") // Exchange code for tokens (without sealing - we want the JWT for desktop) const authResponse = yield* workos @@ -299,7 +299,7 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => const workos = yield* WorkOS const { refreshToken } = payload - const clientId = yield* Config.string("WORKOS_CLIENT_ID").pipe(Effect.orDie) + const clientId = yield* Config.string("WORKOS_CLIENT_ID") // Exchange refresh token for new tokens const authResponse = yield* workos diff --git a/apps/backend/src/routes/bot-commands.http.ts b/apps/backend/src/routes/bot-commands.http.ts index 56f4b3f75..ed2e1e7ad 100644 --- a/apps/backend/src/routes/bot-commands.http.ts +++ b/apps/backend/src/routes/bot-commands.http.ts @@ -262,10 +262,10 @@ export const HttpBotCommandsLive = HttpApiBuilder.group(HazelApi, "bot-commands" ), ) // Execute a bot command (user auth) - .handle("executeBotCommand", ({ path, payload }) => + .handle("executeBotCommand", ({ params, payload }) => Effect.gen(function* () { const currentUser = yield* CurrentUser.Context - const { orgId, botId, commandName } = path + const { orgId, botId, commandName } = params const { channelId, arguments: args } = payload const botRepo = yield* BotRepo @@ -343,7 +343,7 @@ export const HttpBotCommandsLive = HttpApiBuilder.group(HazelApi, "bot-commands" Effect.catchTag("DurableStreamRequestError", (error) => Effect.fail( new BotCommandExecutionError({ - commandName: path.commandName, + commandName: params.commandName, message: "Failed to append command to bot gateway", details: String(error.message), }), @@ -352,10 +352,10 @@ export const HttpBotCommandsLive = HttpApiBuilder.group(HazelApi, "bot-commands" ), ) // Get integration token (bot token auth) - .handle("getIntegrationToken", ({ path }) => + .handle("getIntegrationToken", ({ params }) => Effect.gen(function* () { const bot = yield* validateBotToken - const { orgId, provider } = path + const { orgId, provider } = params // Check provider is in bot's allowedIntegrations const allowed = bot.allowedIntegrations ?? [] @@ -412,23 +412,23 @@ export const HttpBotCommandsLive = HttpApiBuilder.group(HazelApi, "bot-commands" ), ), Effect.catchTag("TokenNotFoundError", () => - Effect.fail(new IntegrationNotConnectedError({ provider: path.provider })), + Effect.fail(new IntegrationNotConnectedError({ provider: params.provider })), ), Effect.catchTag("TokenRefreshError", (error) => Effect.fail( new InternalServerError({ - message: `Failed to refresh ${path.provider} token`, + message: `Failed to refresh ${params.provider} token`, detail: String(error.cause), }), ), ), Effect.catchTag("ConnectionNotFoundError", () => - Effect.fail(new IntegrationNotConnectedError({ provider: path.provider })), + Effect.fail(new IntegrationNotConnectedError({ provider: params.provider })), ), Effect.catchTag("IntegrationEncryptionError", (error) => Effect.fail( new InternalServerError({ - message: `Failed to decrypt ${path.provider} token`, + message: `Failed to decrypt ${params.provider} token`, detail: String(error), }), ), @@ -436,7 +436,7 @@ export const HttpBotCommandsLive = HttpApiBuilder.group(HazelApi, "bot-commands" Effect.catchTag("KeyVersionNotFoundError", (error) => Effect.fail( new InternalServerError({ - message: `Encryption key version not found for ${path.provider} token`, + message: `Encryption key version not found for ${params.provider} token`, detail: String(error), }), ), @@ -444,10 +444,10 @@ export const HttpBotCommandsLive = HttpApiBuilder.group(HazelApi, "bot-commands" ), ) // Get enabled integrations (bot token auth) - .handle("getEnabledIntegrations", ({ path }) => + .handle("getEnabledIntegrations", ({ params }) => Effect.gen(function* () { const bot = yield* validateBotToken - const { orgId } = path + const { orgId } = params // Verify bot is installed in this org const installationRepo = yield* BotInstallationRepo diff --git a/apps/backend/src/routes/chat-sync.http.ts b/apps/backend/src/routes/chat-sync.http.ts index b544d8644..f021f4762 100644 --- a/apps/backend/src/routes/chat-sync.http.ts +++ b/apps/backend/src/routes/chat-sync.http.ts @@ -58,9 +58,9 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han const integrationConnectionRepo = yield* IntegrationConnectionRepo return handlers - .handle("createConnection", ({ path, payload }) => + .handle("createConnection", ({ params, payload }) => Effect.gen(function* () { - yield* ensureOrgAccess(path.orgId) + yield* ensureOrgAccess(params.orgId) const currentUser = yield* CurrentUser.Context const integrationConnectionId = yield* Effect.gen(function* () { @@ -73,13 +73,13 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han } const integrationOption = yield* integrationConnectionRepo.findOrgConnection( - path.orgId, + params.orgId, "discord", ) if (Option.isNone(integrationOption) || integrationOption.value.status !== "active") { return yield* Effect.fail( new ChatSyncIntegrationNotConnectedError({ - organizationId: path.orgId, + organizationId: params.orgId, provider: "discord", }), ) @@ -89,14 +89,14 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han }) const existing = yield* connectionRepo.findByProviderAndWorkspace( - path.orgId, + params.orgId, payload.provider, payload.externalWorkspaceId, ) if (Option.isSome(existing)) { return yield* Effect.fail( new ChatSyncConnectionExistsError({ - organizationId: path.orgId, + organizationId: params.orgId, provider: payload.provider, externalWorkspaceId: payload.externalWorkspaceId, }), @@ -104,7 +104,7 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han } const [connection] = yield* connectionRepo.insert({ - organizationId: path.orgId, + organizationId: params.orgId, integrationConnectionId, provider: payload.provider, externalWorkspaceId: payload.externalWorkspaceId, @@ -131,10 +131,10 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han ), ), ) - .handle("listConnections", ({ path }) => + .handle("listConnections", ({ params }) => Effect.gen(function* () { - yield* ensureOrgAccess(path.orgId) - const connections = yield* connectionRepo.findByOrganization(path.orgId) + yield* ensureOrgAccess(params.orgId) + const connections = yield* connectionRepo.findByOrganization(params.orgId) return new ChatSyncConnectionListResponse({ data: connections }) }).pipe( Effect.catchTag("DatabaseError", (error) => @@ -144,22 +144,22 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han ), ), ) - .handle("deleteConnection", ({ path }) => + .handle("deleteConnection", ({ params }) => Effect.gen(function* () { - const connectionOption = yield* connectionRepo.findById(path.syncConnectionId) + const connectionOption = yield* connectionRepo.findById(params.syncConnectionId) if (Option.isNone(connectionOption)) { return yield* Effect.fail( new ChatSyncConnectionNotFoundError({ - syncConnectionId: path.syncConnectionId, + syncConnectionId: params.syncConnectionId, }), ) } const connection = connectionOption.value yield* ensureOrgAccess(connection.organizationId) - yield* connectionRepo.softDelete(path.syncConnectionId) + yield* connectionRepo.softDelete(params.syncConnectionId) - const links = yield* channelLinkRepo.findBySyncConnection(path.syncConnectionId) + const links = yield* channelLinkRepo.findBySyncConnection(params.syncConnectionId) yield* Effect.forEach(links, (link) => channelLinkRepo.softDelete(link.id), { concurrency: 10, }) @@ -174,13 +174,13 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han ), ), ) - .handle("createChannelLink", ({ path, payload }) => + .handle("createChannelLink", ({ params, payload }) => Effect.gen(function* () { - const connectionOption = yield* connectionRepo.findById(path.syncConnectionId) + const connectionOption = yield* connectionRepo.findById(params.syncConnectionId) if (Option.isNone(connectionOption)) { return yield* Effect.fail( new ChatSyncConnectionNotFoundError({ - syncConnectionId: path.syncConnectionId, + syncConnectionId: params.syncConnectionId, }), ) } @@ -188,13 +188,13 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han yield* ensureOrgAccess(connection.organizationId) const existingHazel = yield* channelLinkRepo.findByHazelChannel( - path.syncConnectionId, + params.syncConnectionId, payload.hazelChannelId, ) if (Option.isSome(existingHazel)) { return yield* Effect.fail( new ChatSyncChannelLinkExistsError({ - syncConnectionId: path.syncConnectionId, + syncConnectionId: params.syncConnectionId, hazelChannelId: payload.hazelChannelId, externalChannelId: payload.externalChannelId, }), @@ -202,13 +202,13 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han } const existingExternal = yield* channelLinkRepo.findByExternalChannel( - path.syncConnectionId, + params.syncConnectionId, payload.externalChannelId, ) if (Option.isSome(existingExternal)) { return yield* Effect.fail( new ChatSyncChannelLinkExistsError({ - syncConnectionId: path.syncConnectionId, + syncConnectionId: params.syncConnectionId, hazelChannelId: payload.hazelChannelId, externalChannelId: payload.externalChannelId, }), @@ -216,7 +216,7 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han } const [link] = yield* channelLinkRepo.insert({ - syncConnectionId: path.syncConnectionId, + syncConnectionId: params.syncConnectionId, hazelChannelId: payload.hazelChannelId, externalChannelId: payload.externalChannelId, externalChannelName: payload.externalChannelName ?? null, @@ -244,20 +244,20 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han ), ), ) - .handle("listChannelLinks", ({ path }) => + .handle("listChannelLinks", ({ params }) => Effect.gen(function* () { - const connectionOption = yield* connectionRepo.findById(path.syncConnectionId) + const connectionOption = yield* connectionRepo.findById(params.syncConnectionId) if (Option.isNone(connectionOption)) { return yield* Effect.fail( new ChatSyncConnectionNotFoundError({ - syncConnectionId: path.syncConnectionId, + syncConnectionId: params.syncConnectionId, }), ) } const connection = connectionOption.value yield* ensureOrgAccess(connection.organizationId) - const links = yield* channelLinkRepo.findBySyncConnection(path.syncConnectionId) + const links = yield* channelLinkRepo.findBySyncConnection(params.syncConnectionId) return new ChatSyncChannelLinkListResponse({ data: links }) }).pipe( Effect.catchTag("DatabaseError", (error) => @@ -267,13 +267,13 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han ), ), ) - .handle("deleteChannelLink", ({ path }) => + .handle("deleteChannelLink", ({ params }) => Effect.gen(function* () { - const linkOption = yield* channelLinkRepo.findById(path.syncChannelLinkId) + const linkOption = yield* channelLinkRepo.findById(params.syncChannelLinkId) if (Option.isNone(linkOption)) { return yield* Effect.fail( new ChatSyncChannelLinkNotFoundError({ - syncChannelLinkId: path.syncChannelLinkId, + syncChannelLinkId: params.syncChannelLinkId, }), ) } @@ -289,7 +289,7 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han } yield* ensureOrgAccess(connectionOption.value.organizationId) - yield* channelLinkRepo.softDelete(path.syncChannelLinkId) + yield* channelLinkRepo.softDelete(params.syncChannelLinkId) const txid = yield* generateTransactionId() return new ChatSyncDeleteResponse({ transactionId: txid }) }).pipe( diff --git a/apps/backend/src/routes/incoming-webhooks.http.ts b/apps/backend/src/routes/incoming-webhooks.http.ts index c5381bb8e..5da015f23 100644 --- a/apps/backend/src/routes/incoming-webhooks.http.ts +++ b/apps/backend/src/routes/incoming-webhooks.http.ts @@ -48,9 +48,9 @@ const convertEmbedToDb = (embed: MessageEmbed.MessageEmbed): DbMessageEmbed => ( export const HttpIncomingWebhookLive = HttpApiBuilder.group(HazelApi, "incoming-webhooks", (handlers) => handlers - .handle("execute", ({ path, payload }) => + .handle("execute", ({ params, payload }) => Effect.gen(function* () { - const { webhookId, token } = path + const { webhookId, token } = params const db = yield* Database.Database const webhookRepo = yield* ChannelWebhookRepo const messageRepo = yield* MessageRepo @@ -164,9 +164,9 @@ export const HttpIncomingWebhookLive = HttpApiBuilder.group(HazelApi, "incoming- }), ), ) - .handle("executeOpenStatus", ({ path, payload }) => + .handle("executeOpenStatus", ({ params, payload }) => Effect.gen(function* () { - const { webhookId, token } = path + const { webhookId, token } = params const db = yield* Database.Database const webhookRepo = yield* ChannelWebhookRepo const messageRepo = yield* MessageRepo @@ -267,9 +267,9 @@ export const HttpIncomingWebhookLive = HttpApiBuilder.group(HazelApi, "incoming- }), ), ) - .handle("executeRailway", ({ path, payload }) => + .handle("executeRailway", ({ params, payload }) => Effect.gen(function* () { - const { webhookId, token } = path + const { webhookId, token } = params const db = yield* Database.Database const webhookRepo = yield* ChannelWebhookRepo const messageRepo = yield* MessageRepo diff --git a/apps/backend/src/routes/integration-commands.http.ts b/apps/backend/src/routes/integration-commands.http.ts index c662054d4..c9ac4ba88 100644 --- a/apps/backend/src/routes/integration-commands.http.ts +++ b/apps/backend/src/routes/integration-commands.http.ts @@ -8,9 +8,9 @@ import { HazelApi } from "../api" export const HttpIntegrationCommandLive = HttpApiBuilder.group(HazelApi, "integration-commands", (handlers) => handlers // Get all available commands for the current organization's installed bots - .handle("getAvailableCommands", ({ path }) => + .handle("getAvailableCommands", ({ params }) => Effect.gen(function* () { - const { orgId } = path + const { orgId } = params const botInstallationRepo = yield* BotInstallationRepo const botCommandRepo = yield* BotCommandRepo diff --git a/apps/backend/src/routes/integration-resources.http.ts b/apps/backend/src/routes/integration-resources.http.ts index f51f7253f..13048bf0d 100644 --- a/apps/backend/src/routes/integration-resources.http.ts +++ b/apps/backend/src/routes/integration-resources.http.ts @@ -47,10 +47,10 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( "integration-resources", (handlers) => handlers - .handle("fetchLinearIssue", ({ path, urlParams }) => + .handle("fetchLinearIssue", ({ params, query }) => Effect.gen(function* () { - const { orgId } = path - const { url } = urlParams + const { orgId } = params + const { url } = query // Parse the Linear issue URL const parsed = parseLinearIssueUrl(url) @@ -115,7 +115,7 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( LinearApiError: (error: LinearApiError) => Effect.fail( new IntegrationResourceError({ - url: urlParams.url, + url: query.url, message: error.message, provider: "linear", }), @@ -123,7 +123,7 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( LinearRateLimitError: (error: LinearRateLimitError) => Effect.fail( new IntegrationResourceError({ - url: urlParams.url, + url: query.url, message: error.retryAfter ? `Rate limit exceeded. Try again in ${error.retryAfter} seconds.` : error.message, @@ -133,7 +133,7 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( LinearIssueNotFoundError: (error: LinearIssueNotFoundError) => Effect.fail( new ResourceNotFoundError({ - url: urlParams.url, + url: query.url, message: `Issue not found: ${error.issueId}`, }), ), @@ -156,10 +156,10 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( }), ), ) - .handle("fetchGitHubPR", ({ path, urlParams }) => + .handle("fetchGitHubPR", ({ params, query }) => Effect.gen(function* () { - const { orgId } = path - const { url } = urlParams + const { orgId } = params + const { url } = query // Parse the GitHub PR URL const parsed = GitHub.parseGitHubPRUrl(url) @@ -205,7 +205,7 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( Effect.catchTag("GitHubPRNotFoundError", (error) => Effect.fail( new ResourceNotFoundError({ - url: urlParams.url, + url: query.url, message: `PR not found: ${error.owner}/${error.repo}#${error.number}`, }), ), @@ -214,7 +214,7 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( Effect.catchTag("GitHubApiError", (error) => Effect.fail( new IntegrationResourceError({ - url: urlParams.url, + url: query.url, message: error.message, provider: "github", }), @@ -223,7 +223,7 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( Effect.catchTag("GitHubRateLimitError", (error) => Effect.fail( new IntegrationResourceError({ - url: urlParams.url, + url: query.url, message: error.retryAfter ? `Rate limit exceeded. Try again in ${error.retryAfter} seconds.` : error.message, @@ -259,7 +259,7 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( Effect.catchTag("GitHubRateLimitError", (error) => Effect.fail( new IntegrationResourceError({ - url: urlParams.url, + url: query.url, message: error.message, provider: "github", }), @@ -268,7 +268,7 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( Effect.catchTag("GitHubApiError", (error) => Effect.fail( new IntegrationResourceError({ - url: urlParams.url, + url: query.url, message: error.message, provider: "github", }), @@ -285,8 +285,8 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( ), ), ) - .handle("getGitHubRepositories", ({ path, urlParams }) => - handleGetGitHubRepositories(path, urlParams).pipe( + .handle("getGitHubRepositories", ({ params, query }) => + handleGetGitHubRepositories(params, query).pipe( Effect.catchTags({ DatabaseError: (error) => Effect.fail( @@ -308,8 +308,8 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( }), ), ) - .handle("getDiscordGuilds", ({ path }) => - handleGetDiscordGuilds(path).pipe( + .handle("getDiscordGuilds", ({ params }) => + handleGetDiscordGuilds(params).pipe( Effect.catchTags({ DatabaseError: (error) => Effect.fail( @@ -331,8 +331,8 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( }), ), ) - .handle("getDiscordGuildChannels", ({ path }) => - handleGetDiscordGuildChannels(path).pipe( + .handle("getDiscordGuildChannels", ({ params }) => + handleGetDiscordGuildChannels(params).pipe( Effect.catchTags({ DatabaseError: (error) => Effect.fail( @@ -351,10 +351,10 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( */ const handleGetGitHubRepositories = Effect.fn("integration-resources.getGitHubRepositories")(function* ( path: { orgId: OrganizationId }, - urlParams: { page: number; perPage: number }, + query: { page: number; perPage: number }, ) { - const { orgId } = path - const { page, perPage } = urlParams + const { orgId } = params + const { page, perPage } = query const connectionRepo = yield* IntegrationConnectionRepo const tokenService = yield* IntegrationTokenService @@ -430,7 +430,7 @@ const getActiveDiscordConnection = Effect.fn("integration-resources.getActiveDis const handleGetDiscordGuilds = Effect.fn("integration-resources.getDiscordGuilds")(function* (path: { orgId: OrganizationId }) { - const { orgId } = path + const { orgId } = params const tokenService = yield* IntegrationTokenService const connection = yield* getActiveDiscordConnection(orgId) const accessToken = yield* tokenService.getValidAccessToken(connection.id) @@ -452,7 +452,7 @@ const handleGetDiscordGuilds = Effect.fn("integration-resources.getDiscordGuilds const handleGetDiscordGuildChannels = Effect.fn("integration-resources.getDiscordGuildChannels")( function* (path: { orgId: OrganizationId; guildId: string }) { - const { orgId, guildId } = path + const { orgId, guildId } = params yield* getActiveDiscordConnection(orgId) const botToken = yield* Config.redacted("DISCORD_BOT_TOKEN").pipe( diff --git a/apps/backend/src/routes/integrations.http.test.ts b/apps/backend/src/routes/integrations.http.test.ts index d65b74bc6..47f459356 100644 --- a/apps/backend/src/routes/integrations.http.test.ts +++ b/apps/backend/src/routes/integrations.http.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@effect/vitest" import { CraftApiError, CraftNotFoundError, CraftRateLimitError } from "@hazel/integrations/craft" -import { Effect, Either } from "effect" +import { Effect, Result } from "effect" import { mapCraftConnectApiKeyError, parseOAuthStateParam, @@ -8,9 +8,9 @@ import { } from "./integrations.http.ts" const expectInvalidBaseUrl = async (url: string) => { - const result = await Effect.runPromise(validateCraftBaseUrl(url).pipe(Effect.either)) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + const result = await Effect.runPromise(validateCraftBaseUrl(url).pipe(Effect.result)) + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(result.left._tag).toBe("InvalidApiKeyError") } } @@ -64,12 +64,12 @@ describe("integration connect API key helpers", () => { describe("validateCraftBaseUrl", () => { it("accepts and normalizes a valid Craft connect URL", async () => { const result = await Effect.runPromise( - validateCraftBaseUrl("https://connect.craft.do/links/link_123/api/v1/").pipe(Effect.either), + validateCraftBaseUrl("https://connect.craft.do/links/link_123/api/v1/").pipe(Effect.result), ) - expect(Either.isRight(result)).toBe(true) - if (Either.isRight(result)) { - expect(result.right).toBe("https://connect.craft.do/links/link_123/api/v1") + expect(Result.isSuccess(result)).toBe(true) + if (Result.isSuccess(result)) { + expect(result.value).toBe("https://connect.craft.do/links/link_123/api/v1") } }) diff --git a/apps/backend/src/routes/integrations.http.ts b/apps/backend/src/routes/integrations.http.ts index b12e3766a..c0b5da702 100644 --- a/apps/backend/src/routes/integrations.http.ts +++ b/apps/backend/src/routes/integrations.http.ts @@ -249,8 +249,7 @@ const makeOAuthSessionCookie = ( Effect.try({ try: () => Cookies.unsafeMakeCookie(name, value, { - domain: options.cookieDomain, - path: "/", + domain: options.cookieDomain, params: "/", httpOnly: true, secure: options.secure, sameSite: "lax", @@ -265,8 +264,7 @@ const makeOAuthSessionCookie = ( const expireOAuthSessionCookie = (name: string, options: { cookieDomain: string; secure: boolean }) => HttpServerResponse.expireCookie(name, { - domain: options.cookieDomain, - path: "/", + domain: options.cookieDomain, params: "/", httpOnly: true, secure: options.secure, sameSite: "lax", @@ -281,11 +279,11 @@ const handleGetOAuthUrl = Effect.fn("integrations.getOAuthUrl")(function* ( orgId: OrganizationId provider: IntegrationConnection.IntegrationProvider }, - urlParams: { level?: IntegrationConnection.ConnectionLevel }, + query: { level?: IntegrationConnection.ConnectionLevel }, ) { const currentUser = yield* CurrentUser.Context const { orgId, provider } = path - const level = urlParams.level ?? "organization" + const level = query.level ?? "organization" if (!currentUser.organizationId || currentUser.organizationId !== orgId) { return yield* Effect.fail( @@ -308,8 +306,8 @@ const handleGetOAuthUrl = Effect.fn("integrations.getOAuthUrl")(function* ( ), ) - const frontendUrl = yield* Config.string("FRONTEND_URL").pipe(Effect.orDie) - const cookieDomain = yield* Config.string("WORKOS_COOKIE_DOMAIN").pipe(Effect.orDie) + const frontendUrl = yield* Config.string("FRONTEND_URL") + const cookieDomain = yield* Config.string("WORKOS_COOKIE_DOMAIN") // Get org slug for redirect URL const orgRepo = yield* OrganizationRepo @@ -335,7 +333,7 @@ const handleGetOAuthUrl = Effect.fn("integrations.getOAuthUrl")(function* ( // Determine environment from NODE_ENV config // Local dev uses "local" so production can redirect callbacks back to localhost - const nodeEnv = yield* Config.string("NODE_ENV").pipe(Config.withDefault("production"), Effect.orDie) + const nodeEnv = yield* Config.string("NODE_ENV").pipe(Config.withDefault("production")) const environment = nodeEnv === "development" ? "local" : "production" const cookieSecure = nodeEnv !== "development" @@ -436,7 +434,7 @@ const OAuthSessionState = Schema.Struct({ */ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( path: { provider: IntegrationConnection.IntegrationProvider }, - urlParams: { + query: { code?: string state?: string guild_id?: string @@ -446,14 +444,14 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( }, ) { const { provider } = path - const { code, state: encodedState, installation_id, setup_action, guild_id, permissions } = urlParams + const { code, state: encodedState, installation_id, setup_action, guild_id, permissions } = query // Get request to read cookies const request = yield* HttpServerRequest.HttpServerRequest const sessionCookieName = `${OAUTH_SESSION_COOKIE_PREFIX}${provider}` const sessionCookie = request.cookies[sessionCookieName] - const cookieDomain = yield* Config.string("WORKOS_COOKIE_DOMAIN").pipe(Effect.orDie) - const nodeEnv = yield* Config.string("NODE_ENV").pipe(Config.withDefault("production"), Effect.orDie) + const cookieDomain = yield* Config.string("WORKOS_COOKIE_DOMAIN") + const nodeEnv = yield* Config.string("NODE_ENV").pipe(Config.withDefault("production")) const cookieSecure = nodeEnv !== "development" yield* Effect.logInfo("OAuth callback received", { @@ -540,7 +538,7 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( if (!parsedState && installation_id && setup_action === "update") { const connectionRepo = yield* IntegrationConnectionRepo const orgRepo = yield* OrganizationRepo - const frontendUrl = yield* Config.string("FRONTEND_URL").pipe(Effect.orDie) + const frontendUrl = yield* Config.string("FRONTEND_URL") yield* Effect.logInfo("GitHub update callback - looking up by installation ID", { event: "integration_callback_installation_lookup", @@ -707,10 +705,10 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( schedule: oauthRetrySchedule, while: isRetryableError, }), - Effect.either, + Effect.result, ) - if (tokensResult._tag === "Left") { + if (tokensResult._tag === "Failure") { const error = tokensResult.left yield* Effect.logError("OAuth token exchange failed", { event: "integration_token_exchange_failed", @@ -721,7 +719,7 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( return redirectWithError("token_exchange_failed") } - const tokens = tokensResult.right + const tokens = tokensResult.value yield* Effect.logInfo("OAuth token exchange succeeded", { event: "integration_token_exchange_success", provider, @@ -740,10 +738,10 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( schedule: oauthRetrySchedule, while: isRetryableError, }), - Effect.either, + Effect.result, ) - if (accountInfoResult._tag === "Left") { + if (accountInfoResult._tag === "Failure") { const error = accountInfoResult.left yield* Effect.logError("OAuth account info fetch failed", { event: "integration_account_info_failed", @@ -753,7 +751,7 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( return redirectWithError("account_info_failed") } - const accountInfo = accountInfoResult.right + const accountInfo = accountInfoResult.value yield* Effect.logDebug("OAuth account info fetch succeeded", { event: "integration_account_info_success", provider, @@ -812,9 +810,9 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( lastUsedAt: null, deletedAt: null, }) - ).pipe(Effect.either) + ).pipe(Effect.result) - if (connectionResult._tag === "Left") { + if (connectionResult._tag === "Failure") { yield* Effect.logError("OAuth database upsert failed", { event: "integration_db_upsert_failed", provider, @@ -823,7 +821,7 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( return redirectWithError("db_error") } - const connection = connectionResult.right + const connection = connectionResult.value yield* Effect.logDebug("OAuth database upsert succeeded", { event: "integration_db_upsert_success", provider, @@ -844,9 +842,9 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( expiresAt: tokens.expiresAt, scope: tokens.scope, }) - .pipe(Effect.either) + .pipe(Effect.result) - if (storeResult._tag === "Left") { + if (storeResult._tag === "Failure") { yield* Effect.logError("OAuth token storage failed", { event: "integration_token_storage_failed", provider, @@ -876,9 +874,9 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( externalAccountId: accountInfo.externalAccountId, externalAccountName: accountInfo.externalAccountName, }) - .pipe(Effect.either) + .pipe(Effect.result) - if (reconcileResult._tag === "Left") { + if (reconcileResult._tag === "Failure") { yield* Effect.logWarning( "Failed to re-attribute historical external messages after account link", { @@ -979,7 +977,7 @@ const handleConnectApiKey = Effect.fn("integrations.connectApiKey")(function* ( baseUrlHost: parsedBaseUrl.hostname, baseUrlPath: parsedBaseUrl.pathname, ...craftConnectApiKeyErrorLogFields(error), - }).pipe(Effect.zipRight(Effect.fail(mapCraftConnectApiKeyError(error)))), + }).pipe(Effect.andThen(Effect.fail(mapCraftConnectApiKeyError(error)))), CraftNotFoundError: (error) => Effect.logWarning("Craft API key validation failed", { event: "craft_api_key_validation_failed", @@ -988,7 +986,7 @@ const handleConnectApiKey = Effect.fn("integrations.connectApiKey")(function* ( baseUrlHost: parsedBaseUrl.hostname, baseUrlPath: parsedBaseUrl.pathname, ...craftConnectApiKeyErrorLogFields(error), - }).pipe(Effect.zipRight(Effect.fail(mapCraftConnectApiKeyError(error)))), + }).pipe(Effect.andThen(Effect.fail(mapCraftConnectApiKeyError(error)))), CraftRateLimitError: (error) => Effect.logWarning("Craft API key validation failed", { event: "craft_api_key_validation_failed", @@ -997,7 +995,7 @@ const handleConnectApiKey = Effect.fn("integrations.connectApiKey")(function* ( baseUrlHost: parsedBaseUrl.hostname, baseUrlPath: parsedBaseUrl.pathname, ...craftConnectApiKeyErrorLogFields(error), - }).pipe(Effect.zipRight(Effect.fail(mapCraftConnectApiKeyError(error)))), + }).pipe(Effect.andThen(Effect.fail(mapCraftConnectApiKeyError(error)))), }), ) externalAccountName = spaceInfo.name ?? "Craft Space" @@ -1067,12 +1065,12 @@ const handleGetConnectionStatus = Effect.fn("integrations.getConnectionStatus")( orgId: OrganizationId provider: IntegrationConnection.IntegrationProvider }, - urlParams: { level?: IntegrationConnection.ConnectionLevel }, + query: { level?: IntegrationConnection.ConnectionLevel }, ) { const { orgId, provider } = path const currentUser = yield* CurrentUser.Context const connectionRepo = yield* IntegrationConnectionRepo - const level = urlParams.level ?? "organization" + const level = query.level ?? "organization" if (!currentUser.organizationId || currentUser.organizationId !== orgId) { return yield* Effect.fail( @@ -1117,14 +1115,14 @@ const handleDisconnect = Effect.fn("integrations.disconnect")(function* ( orgId: OrganizationId provider: IntegrationConnection.IntegrationProvider }, - urlParams: { level?: IntegrationConnection.ConnectionLevel }, + query: { level?: IntegrationConnection.ConnectionLevel }, ) { const { orgId, provider } = path const currentUser = yield* CurrentUser.Context const connectionRepo = yield* IntegrationConnectionRepo const tokenService = yield* IntegrationTokenService const chatSyncAttributionReconciler = yield* ChatSyncAttributionReconciler - const level = urlParams.level ?? "organization" + const level = query.level ?? "organization" if (!currentUser.organizationId || currentUser.organizationId !== orgId) { return yield* Effect.fail( @@ -1160,9 +1158,9 @@ const handleDisconnect = Effect.fn("integrations.disconnect")(function* ( externalAccountId, externalAccountName: connection.externalAccountName, }) - .pipe(Effect.either) + .pipe(Effect.result) - if (reconcileResult._tag === "Left") { + if (reconcileResult._tag === "Failure") { yield* Effect.logWarning( "Failed to re-attribute historical external messages after account unlink", { @@ -1197,9 +1195,9 @@ const handleDisconnect = Effect.fn("integrations.disconnect")(function* ( export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations", (handlers) => handlers - .handle("getOAuthUrl", ({ path, urlParams }) => handleGetOAuthUrl(path, urlParams)) - .handle("oauthCallback", ({ path, urlParams }) => - handleOAuthCallback(path, urlParams).pipe( + .handle("getOAuthUrl", ({ params, query }) => handleGetOAuthUrl(path, query)) + .handle("oauthCallback", ({ params, query }) => + handleOAuthCallback(path, query).pipe( Effect.catchTag("DatabaseError", (error) => Effect.fail( new InternalServerError({ @@ -1210,7 +1208,7 @@ export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations" ), ), ) - .handle("connectApiKey", ({ path, payload }) => + .handle("connectApiKey", ({ params, payload }) => handleConnectApiKey(path, payload).pipe( Effect.catchTags({ DatabaseError: (error) => @@ -1237,8 +1235,8 @@ export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations" }), ), ) - .handle("getConnectionStatus", ({ path, urlParams }) => - handleGetConnectionStatus(path, urlParams).pipe( + .handle("getConnectionStatus", ({ params, query }) => + handleGetConnectionStatus(path, query).pipe( Effect.catchTag("DatabaseError", (error) => Effect.fail( new InternalServerError({ @@ -1249,8 +1247,8 @@ export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations" ), ), ) - .handle("disconnect", ({ path, urlParams }) => - handleDisconnect(path, urlParams).pipe( + .handle("disconnect", ({ params, query }) => + handleDisconnect(path, query).pipe( Effect.catchTag("DatabaseError", (error) => Effect.fail( new InternalServerError({ diff --git a/apps/backend/src/routes/internal.http.ts b/apps/backend/src/routes/internal.http.ts index 48d7b2716..292b0d5a8 100644 --- a/apps/backend/src/routes/internal.http.ts +++ b/apps/backend/src/routes/internal.http.ts @@ -22,8 +22,7 @@ export const HttpInternalLive = HttpApiBuilder.group(HazelApi, "internal", (hand const request = yield* HttpServerRequest.HttpServerRequest // Optionally verify internal secret for server-to-server auth - const internalSecret = yield* Config.string("INTERNAL_SECRET").pipe( - Effect.option, + const internalSecret = yield* Effect.option(Config.string("INTERNAL_SECRET")).pipe( Effect.map(Option.getOrUndefined), ) diff --git a/apps/backend/src/routes/klipy.http.ts b/apps/backend/src/routes/klipy.http.ts index 9a19244ea..8341a3fd2 100644 --- a/apps/backend/src/routes/klipy.http.ts +++ b/apps/backend/src/routes/klipy.http.ts @@ -62,8 +62,7 @@ const KlipyRawCategoriesResponse = Schema.Struct({ const fetchKlipy = ( httpClient: HttpClient.HttpClient, - apiKey: string, - path: string, + apiKey: string, params: string, params: Record, ) => { const searchParams = new URLSearchParams(params) @@ -98,17 +97,17 @@ const fetchKlipy = ( export const HttpKlipyLive = HttpApiBuilder.group(HazelApi, "klipy", (handlers) => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient - const apiKeyRedacted = yield* Config.redacted("KLIPY_API_KEY").pipe(Effect.orDie) + const apiKeyRedacted = yield* Config.redacted("KLIPY_API_KEY") const apiKey = Redacted.value(apiKeyRedacted) return handlers - .handle("trending", ({ urlParams }) => + .handle("trending", ({ query }) => Effect.gen(function* () { const raw = yield* fetchKlipy(httpClient, apiKey, "/gifs/trending", { - page: String(urlParams.page), - per_page: String(urlParams.per_page), + page: String(query.page), + per_page: String(query.per_page), }) - const parsed = yield* Schema.decodeUnknown(KlipyRawSearchResponse)(raw).pipe( + const parsed = yield* Schema.decodeUnknownEffect(KlipyRawSearchResponse)(raw).pipe( Effect.mapError( (error) => new KlipyApiError({ @@ -119,14 +118,14 @@ export const HttpKlipyLive = HttpApiBuilder.group(HazelApi, "klipy", (handlers) return parsed.data }), ) - .handle("search", ({ urlParams }) => + .handle("search", ({ query }) => Effect.gen(function* () { const raw = yield* fetchKlipy(httpClient, apiKey, "/gifs/search", { - q: urlParams.q, - page: String(urlParams.page), - per_page: String(urlParams.per_page), + q: query.q, + page: String(query.page), + per_page: String(query.per_page), }) - const parsed = yield* Schema.decodeUnknown(KlipyRawSearchResponse)(raw).pipe( + const parsed = yield* Schema.decodeUnknownEffect(KlipyRawSearchResponse)(raw).pipe( Effect.mapError( (error) => new KlipyApiError({ @@ -142,7 +141,7 @@ export const HttpKlipyLive = HttpApiBuilder.group(HazelApi, "klipy", (handlers) const raw = yield* fetchKlipy(httpClient, apiKey, "/gifs/categories", { locale: "en_US", }) - const parsed = yield* Schema.decodeUnknown(KlipyRawCategoriesResponse)(raw).pipe( + const parsed = yield* Schema.decodeUnknownEffect(KlipyRawCategoriesResponse)(raw).pipe( Effect.mapError( (error) => new KlipyApiError({ diff --git a/apps/backend/src/rpc/handlers/channels.ts b/apps/backend/src/rpc/handlers/channels.ts index 00f7fe02f..ca93e1e36 100644 --- a/apps/backend/src/rpc/handlers/channels.ts +++ b/apps/backend/src/rpc/handlers/channels.ts @@ -476,7 +476,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( const originalMessageId = originalMessageResult[0]!.id - const clusterUrl = yield* Config.string("CLUSTER_URL").pipe(Effect.orDie) + const clusterUrl = yield* Config.string("CLUSTER_URL") const client = yield* HttpApiClient.make(Cluster.WorkflowApi, { baseUrl: clusterUrl, }) diff --git a/apps/backend/src/rpc/handlers/connect-shares.test.ts b/apps/backend/src/rpc/handlers/connect-shares.test.ts index 3975afacb..ff0796645 100644 --- a/apps/backend/src/rpc/handlers/connect-shares.test.ts +++ b/apps/backend/src/rpc/handlers/connect-shares.test.ts @@ -23,11 +23,11 @@ describe("connect-shares helpers", () => { providedOrgId: GUEST_ORG_ID, target: { kind: "slug", value: "other-workspace" }, findBySlug: () => Effect.succeed(Option.some({ id: OTHER_GUEST_ORG_ID })), - }).pipe(Effect.either) as Effect.Effect, + }).pipe(Effect.result) as Effect.Effect, ) - expect(result._tag).toBe("Left") - if (result._tag === "Left") { + expect(result._tag).toBe("Failure") + if (result._tag === "Failure") { expect(result.left).toBeInstanceOf(ConnectWorkspaceNotFoundError) } }) @@ -42,11 +42,11 @@ describe("connect-shares helpers", () => { }, guestOrganizationId: OTHER_GUEST_ORG_ID, findBySlug: () => Effect.succeed(Option.some({ id: GUEST_ORG_ID })), - }).pipe(Effect.either) as Effect.Effect, + }).pipe(Effect.result) as Effect.Effect, ) - expect(result._tag).toBe("Left") - if (result._tag === "Left") { + expect(result._tag).toBe("Failure") + if (result._tag === "Failure") { expect(result.left).toBeInstanceOf(ConnectWorkspaceNotFoundError) } }) @@ -61,11 +61,11 @@ describe("connect-shares helpers", () => { }, guestOrganizationId: GUEST_ORG_ID, findBySlug: () => Effect.succeed(Option.none()), - }).pipe(Effect.either) as Effect.Effect, + }).pipe(Effect.result) as Effect.Effect, ) - expect(result._tag).toBe("Left") - if (result._tag === "Left") { + expect(result._tag).toBe("Failure") + if (result._tag === "Failure") { expect(result.left).toBeInstanceOf(ConnectWorkspaceNotFoundError) } }) @@ -75,11 +75,11 @@ describe("connect-shares helpers", () => { assertGuestMemberAddsAllowed({ role: "guest", allowGuestMemberAdds: false, - }).pipe(Effect.either) as Effect.Effect, + }).pipe(Effect.result) as Effect.Effect, ) - expect(result._tag).toBe("Left") - if (result._tag === "Left") { + expect(result._tag).toBe("Failure") + if (result._tag === "Failure") { expect(result.left).toBeInstanceOf(PermissionError) } }) @@ -89,17 +89,17 @@ describe("connect-shares helpers", () => { assertGuestMemberAddsAllowed({ role: "host", allowGuestMemberAdds: false, - }).pipe(Effect.either) as Effect.Effect, + }).pipe(Effect.result) as Effect.Effect, ) const guestResult = await Effect.runPromise( assertGuestMemberAddsAllowed({ role: "guest", allowGuestMemberAdds: true, - }).pipe(Effect.either) as Effect.Effect, + }).pipe(Effect.result) as Effect.Effect, ) - expect(hostResult._tag).toBe("Right") - expect(guestResult._tag).toBe("Right") + expect(hostResult._tag).toBe("Success") + expect(guestResult._tag).toBe("Success") }) it("remaps guest mount unique conflicts to an already-shared error", async () => { @@ -111,11 +111,11 @@ describe("connect-shares helpers", () => { }), guestOrganizationId: GUEST_ORG_ID, findExistingMount: () => Effect.succeed(Option.some({ channelId: GUEST_CHANNEL_ID })), - }).pipe(Effect.either) as Effect.Effect, + }).pipe(Effect.result) as Effect.Effect, ) - expect(result._tag).toBe("Left") - if (result._tag === "Left") { + expect(result._tag).toBe("Failure") + if (result._tag === "Failure") { expect(result.left).toBeInstanceOf(ConnectChannelAlreadySharedError) } }) diff --git a/apps/backend/src/rpc/handlers/connect-shares.ts b/apps/backend/src/rpc/handlers/connect-shares.ts index 6bdd85053..a259df8b3 100644 --- a/apps/backend/src/rpc/handlers/connect-shares.ts +++ b/apps/backend/src/rpc/handlers/connect-shares.ts @@ -202,8 +202,8 @@ export const ConnectShareRpcLive = ConnectShareRpcs.toLayer( return } - const hostAttempt = yield* requireAdminOrOwner(hostOrganizationId).pipe(Effect.either) - if (hostAttempt._tag === "Right") return + const hostAttempt = yield* requireAdminOrOwner(hostOrganizationId).pipe(Effect.result) + if (hostAttempt._tag === "Success") return yield* requireAdminOrOwner(targetOrganizationId) }) diff --git a/apps/backend/src/rpc/middleware/auth.ts b/apps/backend/src/rpc/middleware/auth.ts index 9c64da775..c2cf48ed3 100644 --- a/apps/backend/src/rpc/middleware/auth.ts +++ b/apps/backend/src/rpc/middleware/auth.ts @@ -1,7 +1,7 @@ import { Headers } from "effect/unstable/http" import { BotRepo, UserRepo } from "@hazel/backend-core" import { InvalidBearerTokenError, type CurrentUser, SessionNotProvidedError } from "@hazel/domain" -import { Effect, FiberRef, Layer, Option } from "effect" +import { Effect, Layer, Option } from "effect" import { AuthMiddleware } from "@hazel/domain/rpc" import { type ApiScope, CurrentBotScopes } from "@hazel/domain/scopes" import { SessionManager } from "../../services/session-manager" @@ -74,9 +74,10 @@ export const AuthMiddlewareLive = Layer.effect( const bot = botOption.value - // Set the bot's declared scopes for authorization - const botApiScopes = new Set(bot.scopes ?? []) as ReadonlySet - yield* FiberRef.set(CurrentBotScopes, Option.some(botApiScopes)) + // TODO: v4 migration — Set the bot's declared scopes for authorization + // In v4, CurrentBotScopes is a ServiceMap.Service, needs different pattern + const _botApiScopes = new Set(bot.scopes ?? []) as ReadonlySet + void _botApiScopes // Will be used when service provision is wired // Get the bot's user from users table const userOption = yield* userRepo.findById(bot.userId).pipe( diff --git a/apps/backend/src/rpc/middleware/scope-injection.ts b/apps/backend/src/rpc/middleware/scope-injection.ts index 691852bd2..eddb5cb12 100644 --- a/apps/backend/src/rpc/middleware/scope-injection.ts +++ b/apps/backend/src/rpc/middleware/scope-injection.ts @@ -16,6 +16,6 @@ export const ScopeInjectionMiddlewareLive = Layer.succeed( if (Option.isNone(scopesOption)) { return next } - return Effect.locally(CurrentRpcScopes, scopesOption.value)(next) + return Effect.provideService(next, CurrentRpcScopes, scopesOption.value) }), ) diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index f68bc77e9..7c039d991 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -221,7 +221,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() const getAttachmentPublicUrlBase = Effect.fn("discordSyncWorker.getAttachmentPublicUrlBase")( function* () { - const configuredBaseUrl = yield* Config.string("S3_PUBLIC_URL").pipe(Effect.option) + const configuredBaseUrl = yield* Effect.option(Config.string("S3_PUBLIC_URL")) if (Option.isNone(configuredBaseUrl) || configuredBaseUrl.value.trim().length === 0) { return yield* Effect.fail( new DiscordSyncConfigurationError({ @@ -500,7 +500,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() return currentConfig } - const botTokenOption = yield* Config.redacted("DISCORD_BOT_TOKEN").pipe(Effect.option) + const botTokenOption = yield* Effect.option(Config.redacted("DISCORD_BOT_TOKEN")) if (Option.isNone(botTokenOption)) { return Option.none() } @@ -1277,9 +1277,9 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() const result = yield* syncHazelMessageToProvider( syncConnectionId, unsyncedMessage.id, - ).pipe(Effect.either) - if (result._tag === "Right") { - if (result.right.status === "synced") { + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.value.status === "synced") { sent++ } else { skipped++ @@ -1749,9 +1749,9 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() target.syncConnectionId, hazelMessageId, dedupeKey, - ).pipe(Effect.either) - if (result._tag === "Right") { - if (result.right.status === "synced" || result.right.status === "already_linked") { + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.value.status === "synced" || result.value.status === "already_linked") { synced++ } } else { @@ -1785,9 +1785,9 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() target.syncConnectionId, hazelMessageId, dedupeKey, - ).pipe(Effect.either) - if (result._tag === "Right") { - if (result.right.status === "updated") { + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.value.status === "updated") { synced++ } } else { @@ -1821,9 +1821,9 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() target.syncConnectionId, hazelMessageId, dedupeKey, - ).pipe(Effect.either) - if (result._tag === "Right") { - if (result.right.status === "deleted") { + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.value.status === "deleted") { synced++ } } else { @@ -1858,9 +1858,9 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() target.syncConnectionId, hazelReactionId, dedupeKey, - ).pipe(Effect.either) - if (result._tag === "Right") { - if (result.right.status === "created") { + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.value.status === "created") { synced++ } } else { @@ -1899,9 +1899,9 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() target.syncConnectionId, payload, dedupeKey, - ).pipe(Effect.either) - if (result._tag === "Right") { - if (result.right.status === "deleted") { + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.value.status === "deleted") { synced++ } } else { diff --git a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts index bddd8c0ca..c29837f8a 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts @@ -92,7 +92,7 @@ export class ChatSyncProviderRegistry extends ServiceMap.Service { const result = await runWorkerEffect( DiscordSyncWorker.syncHazelMessageToDiscord(SYNC_CONNECTION_ID, HAZEL_MESSAGE_ID).pipe( Effect.provide(layer), - Effect.either, + Effect.result, ), ) - expect(result._tag).toBe("Left") - if (result._tag === "Left") { + expect(result._tag).toBe("Failure") + if (result._tag === "Failure") { expect((result.left as { _tag?: string })._tag).toBe("DiscordSyncConfigurationError") } } finally { diff --git a/apps/backend/src/services/connect-conversation-service.ts b/apps/backend/src/services/connect-conversation-service.ts index 4ffab4cd3..7ffe2e808 100644 --- a/apps/backend/src/services/connect-conversation-service.ts +++ b/apps/backend/src/services/connect-conversation-service.ts @@ -158,8 +158,8 @@ export class ConnectConversationService extends ServiceMap.Service => pool.connect(), catch: (cause) => new Error(String(cause)), - }).pipe(Effect.either) + }).pipe(Effect.result) - if (reservedResult._tag === "Left") { + if (reservedResult._tag === "Failure") { yield* Effect.logError("Failed to reserve outbox advisory lock connection", { error: String(reservedResult.left), }) yield* Effect.sleep(OUTBOX_LOCK_RETRY_INTERVAL) return yield* campaignForLeadership() } - const reserved = reservedResult.right + const reserved = reservedResult.value const lockResult = yield* Effect.tryPromise({ try: () => @@ -191,9 +191,9 @@ export class MessageOutboxDispatcher extends ServiceMap.Service new Error(String(cause)), - }).pipe(Effect.either) + }).pipe(Effect.result) - if (lockResult._tag === "Left") { + if (lockResult._tag === "Failure") { yield* Effect.logError("Failed to acquire outbox advisory lock", { error: String(lockResult.left), }) @@ -202,7 +202,7 @@ export class MessageOutboxDispatcher extends ServiceMap.Service } + const lockRows = lockResult.value as { rows: Array<{ locked: boolean }> } if (!lockRows.rows[0]?.locked) { yield* Effect.sync(() => reserved.release()) yield* Effect.sleep(OUTBOX_LOCK_RETRY_INTERVAL) diff --git a/apps/backend/src/services/message-side-effect-service.ts b/apps/backend/src/services/message-side-effect-service.ts index 07660dbc1..0b415f237 100644 --- a/apps/backend/src/services/message-side-effect-service.ts +++ b/apps/backend/src/services/message-side-effect-service.ts @@ -17,7 +17,7 @@ export class MessageSideEffectService extends ServiceMap.Service()("OAut } const data = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(OAuthTokenApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(OAuthTokenApiResponse)), Effect.catchTags({ ParseError: (error) => new OAuthHttpError({ @@ -190,7 +190,7 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut } const data = yield* response.json.pipe( - Effect.flatMap(Schema.decodeUnknown(OAuthTokenApiResponse)), + Effect.flatMap(Schema.decodeUnknownEffect(OAuthTokenApiResponse)), Effect.catchTags({ ParseError: (error) => new OAuthHttpError({ diff --git a/apps/backend/src/services/org-resolver.test.ts b/apps/backend/src/services/org-resolver.test.ts index d697f2173..9034a3a3c 100644 --- a/apps/backend/src/services/org-resolver.test.ts +++ b/apps/backend/src/services/org-resolver.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import { ChannelMemberRepo, ChannelRepo, MessageRepo, OrganizationMemberRepo } from "@hazel/backend-core" import { PermissionError } from "@hazel/domain" import type { ChannelId, ChannelMemberId, MessageId, OrganizationId, UserId } from "@hazel/schema" -import { Effect, Either, Layer, Option } from "effect" +import { Effect, Result, Layer, Option } from "effect" import { OrgResolver } from "./org-resolver" import { makeActor, TEST_ORG_ID } from "../policies/policy-test-helpers" import { CurrentUser } from "@hazel/domain" @@ -74,7 +74,7 @@ const runEither = ( actor: CurrentUser.Schema = makeActor(), ) => Effect.runPromise( - effect.pipe(Effect.provide(layer), Effect.provideService(CurrentUser.Context, actor), Effect.either), + effect.pipe(Effect.provide(layer), Effect.provideService(CurrentUser.Context, actor), Effect.result), ) const use = (fn: (resolver: OrgResolver) => Effect.Effect) => @@ -96,7 +96,7 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("denies access for non-members", async () => { @@ -108,8 +108,8 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(PermissionError.is(result.left)).toBe(true) } }) @@ -129,7 +129,7 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("grants access for owner", async () => { @@ -145,7 +145,7 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("denies access for regular member", async () => { @@ -161,8 +161,8 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { expect(PermissionError.is(result.left)).toBe(true) } }) @@ -191,8 +191,8 @@ describe("OrgResolver", () => { adminActor, ) - expect(Either.isRight(ownerResult)).toBe(true) - expect(Either.isLeft(adminResult)).toBe(true) + expect(Result.isSuccess(ownerResult)).toBe(true) + expect(Result.isFailure(adminResult)).toBe(true) }) }) @@ -215,7 +215,7 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("fails for missing channel", async () => { @@ -230,7 +230,7 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) }) @@ -253,7 +253,7 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("allows private channel for admin without membership", async () => { @@ -274,7 +274,7 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("denies private channel for non-admin without channel membership", async () => { @@ -296,7 +296,7 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it("allows private channel for member with channel membership", async () => { @@ -318,7 +318,7 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("allows direct channel only for channel members", async () => { @@ -351,8 +351,8 @@ describe("OrgResolver", () => { outsider, ) - expect(Either.isRight(memberResult)).toBe(true) - expect(Either.isLeft(outsiderResult)).toBe(true) + expect(Result.isSuccess(memberResult)).toBe(true) + expect(Result.isFailure(outsiderResult)).toBe(true) }) it("checks parent channel access for threads", async () => { @@ -382,7 +382,7 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) }) @@ -408,7 +408,7 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it("fails for missing message", async () => { @@ -423,7 +423,7 @@ describe("OrgResolver", () => { layer, actor, ) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) }) }) diff --git a/apps/backend/src/test/message-outbox-repo.test.ts b/apps/backend/src/test/message-outbox-repo.test.ts index 358a0db0f..32574963e 100644 --- a/apps/backend/src/test/message-outbox-repo.test.ts +++ b/apps/backend/src/test/message-outbox-repo.test.ts @@ -83,7 +83,7 @@ describe("MessageOutboxRepo", () => { expect(claimed).toHaveLength(3) expect(claimed.map((event) => event.sequence)).toEqual( - [...claimed.map((event) => event.sequence)].sort((left, right) => left - right), + [...claimed.map((event) => event.sequence)].sort((value, right) => left - right), ) expect(claimed.map((event) => event.eventType)).toEqual([ "message_created", diff --git a/apps/web/src/atoms/emoji-atoms.ts b/apps/web/src/atoms/emoji-atoms.ts index f75487efc..4d1e6f4fa 100644 --- a/apps/web/src/atoms/emoji-atoms.ts +++ b/apps/web/src/atoms/emoji-atoms.ts @@ -11,10 +11,7 @@ const DEFAULT_EMOJIS = ["👍", "❤️", "😂", "🔥", "👀", "🎉"] as con * Schema for emoji usage data * Maps emoji string to usage count */ -const EmojiUsageSchema = Schema.Record({ - key: Schema.String, - value: Schema.Number, -}) +const EmojiUsageSchema = Schema.Record(Schema.String, Schema.Number) export type EmojiUsage = typeof EmojiUsageSchema.Type @@ -24,7 +21,7 @@ export type EmojiUsage = typeof EmojiUsageSchema.Type export const emojiUsageAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "hazel-emoji-usage", - schema: EmojiUsageSchema, + schema: Schema.toCodecIso(EmojiUsageSchema), defaultValue: () => ({}) as EmojiUsage, }) diff --git a/apps/web/src/atoms/feature-discovery-atoms.ts b/apps/web/src/atoms/feature-discovery-atoms.ts index 9953d07e6..df5d1dcab 100644 --- a/apps/web/src/atoms/feature-discovery-atoms.ts +++ b/apps/web/src/atoms/feature-discovery-atoms.ts @@ -8,15 +8,12 @@ const HINT_IDS = ["command-palette", "create-channel"] as const export type HintId = (typeof HINT_IDS)[number] -const DismissedHintsSchema = Schema.Record({ - key: Schema.String, - value: Schema.Boolean, -}) +const DismissedHintsSchema = Schema.Record(Schema.String, Schema.Boolean) export const dismissedHintsAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "hazel-dismissed-hints", - schema: DismissedHintsSchema, + schema: Schema.toCodecIso(DismissedHintsSchema), defaultValue: () => ({}) as Record, }).pipe(Atom.keepAlive) diff --git a/apps/web/src/atoms/hotkey-atoms.ts b/apps/web/src/atoms/hotkey-atoms.ts index 4d11c7c08..5ea8fa66d 100644 --- a/apps/web/src/atoms/hotkey-atoms.ts +++ b/apps/web/src/atoms/hotkey-atoms.ts @@ -12,10 +12,7 @@ import { } from "~/lib/hotkeys/hotkey-registry" import { platformStorageRuntime } from "~/lib/platform-storage" -const HotkeyOverridesSchema = Schema.Record({ - key: Schema.String, - value: Schema.String, -}) +const HotkeyOverridesSchema = Schema.Record(Schema.String, Schema.String) type HotkeyOverrides = typeof HotkeyOverridesSchema.Type @@ -36,7 +33,7 @@ export interface ResolvedHotkeyDefinition extends AppHotkeyDefinition { export const hotkeyOverridesAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "hazel-hotkey-overrides", - schema: HotkeyOverridesSchema, + schema: Schema.toCodecIso(HotkeyOverridesSchema), defaultValue: () => ({}) as HotkeyOverrides, }).pipe(Atom.keepAlive) diff --git a/apps/web/src/atoms/notification-sound-atoms.ts b/apps/web/src/atoms/notification-sound-atoms.ts index 28ccb3c20..ee0df0ed7 100644 --- a/apps/web/src/atoms/notification-sound-atoms.ts +++ b/apps/web/src/atoms/notification-sound-atoms.ts @@ -40,7 +40,7 @@ const DEFAULT_SETTINGS: NotificationSoundSettings = { export const notificationSoundSettingsAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "notification-sound-settings", - schema: Schema.NullOr(NotificationSoundSettingsSchema), + schema: Schema.toCodecIso(Schema.NullOr(NotificationSoundSettingsSchema)), defaultValue: () => DEFAULT_SETTINGS, }) diff --git a/apps/web/src/atoms/panel-atoms.ts b/apps/web/src/atoms/panel-atoms.ts index 09a5956bd..bc800eaf8 100644 --- a/apps/web/src/atoms/panel-atoms.ts +++ b/apps/web/src/atoms/panel-atoms.ts @@ -36,7 +36,7 @@ export const panelWidthAtomFamily = Atom.family((panelType: PanelType) => Atom.kvs({ runtime: platformStorageRuntime, key: `panel_width_${panelType}`, - schema: Schema.NullOr(Schema.Number), + schema: Schema.toCodecIso(Schema.NullOr(Schema.Number)), defaultValue: () => DEFAULT_PANEL_WIDTHS[panelType], }), ) diff --git a/apps/web/src/atoms/react-scan-atoms.ts b/apps/web/src/atoms/react-scan-atoms.ts index b7386cdb3..635cf2039 100644 --- a/apps/web/src/atoms/react-scan-atoms.ts +++ b/apps/web/src/atoms/react-scan-atoms.ts @@ -5,6 +5,6 @@ import { platformStorageRuntime } from "~/lib/platform-storage" export const reactScanEnabledAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "react-scan-enabled", - schema: Schema.NullOr(Schema.Boolean), + schema: Schema.toCodecIso(Schema.NullOr(Schema.Boolean)), defaultValue: () => false, }) diff --git a/apps/web/src/atoms/recent-channels-atom.ts b/apps/web/src/atoms/recent-channels-atom.ts index d5b22a03f..f438e1094 100644 --- a/apps/web/src/atoms/recent-channels-atom.ts +++ b/apps/web/src/atoms/recent-channels-atom.ts @@ -26,6 +26,6 @@ const RecentChannelsSchema = Schema.Array(RecentChannelSchema) export const recentChannelsAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "recentChannels", - schema: RecentChannelsSchema, + schema: Schema.toCodecIso(RecentChannelsSchema), defaultValue: () => [] as RecentChannel[], }).pipe(Atom.keepAlive) diff --git a/apps/web/src/atoms/search-atoms.ts b/apps/web/src/atoms/search-atoms.ts index 6845c01eb..53e652687 100644 --- a/apps/web/src/atoms/search-atoms.ts +++ b/apps/web/src/atoms/search-atoms.ts @@ -38,6 +38,6 @@ const RecentSearchesSchema = Schema.Array(RecentSearchSchema) export const recentSearchesAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "recentSearches", - schema: RecentSearchesSchema, + schema: Schema.toCodecIso(RecentSearchesSchema), defaultValue: () => [] as RecentSearch[], }).pipe(Atom.keepAlive) diff --git a/apps/web/src/atoms/section-collapse-atoms.ts b/apps/web/src/atoms/section-collapse-atoms.ts index 186561e60..b7be15370 100644 --- a/apps/web/src/atoms/section-collapse-atoms.ts +++ b/apps/web/src/atoms/section-collapse-atoms.ts @@ -7,10 +7,7 @@ import { platformStorageRuntime } from "~/lib/platform-storage" * Schema for collapsed sections state * Maps section IDs to their collapsed state */ -const CollapsedSectionsSchema = Schema.Record({ - key: Schema.String, - value: Schema.Boolean, -}) +const CollapsedSectionsSchema = Schema.Record(Schema.String, Schema.Boolean) /** * Atom that stores collapsed state for all sections @@ -19,7 +16,7 @@ const CollapsedSectionsSchema = Schema.Record({ export const collapsedSectionsAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "section_collapsed_state", - schema: CollapsedSectionsSchema, + schema: Schema.toCodecIso(CollapsedSectionsSchema), defaultValue: () => ({}) as Record, }) diff --git a/apps/web/src/atoms/sidebar-atoms.ts b/apps/web/src/atoms/sidebar-atoms.ts index a086f4fc8..01f5009fa 100644 --- a/apps/web/src/atoms/sidebar-atoms.ts +++ b/apps/web/src/atoms/sidebar-atoms.ts @@ -14,7 +14,7 @@ export type SidebarState = "expanded" | "collapsed" export const sidebarOpenAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "sidebar_state", - schema: Schema.NullOr(Schema.Boolean), + schema: Schema.toCodecIso(Schema.NullOr(Schema.Boolean)), defaultValue: () => true, }) diff --git a/apps/web/src/components/channel-settings/add-github-repo-modal.tsx b/apps/web/src/components/channel-settings/add-github-repo-modal.tsx index 952e607ed..246f0cec1 100644 --- a/apps/web/src/components/channel-settings/add-github-repo-modal.tsx +++ b/apps/web/src/components/channel-settings/add-github-repo-modal.tsx @@ -61,7 +61,7 @@ export function AddGitHubRepoModal({ // Fetch repositories const repositoriesResult = useAtomValue( HazelApiClient.query("integration-resources", "getGitHubRepositories", { - path: { orgId: organizationId }, + params: { orgId: organizationId }, urlParams: { page: 1, perPage: 100 }, }), ) @@ -124,7 +124,7 @@ export function AddGitHubRepoModal({ } // Get repositories from result - const repositories = Result.builder(repositoriesResult) + const repositories = AsyncResult.builder(repositoriesResult) .onSuccess((data) => data?.repositories ?? []) .orElse(() => []) @@ -145,7 +145,7 @@ export function AddGitHubRepoModal({ {/* Repository selector */}
- {Result.builder(repositoriesResult) + {AsyncResult.builder(repositoriesResult) .onInitial(() => (
diff --git a/apps/web/src/components/chat-sync/add-channel-link-modal.tsx b/apps/web/src/components/chat-sync/add-channel-link-modal.tsx index 08ae576df..1245bd011 100644 --- a/apps/web/src/components/chat-sync/add-channel-link-modal.tsx +++ b/apps/web/src/components/chat-sync/add-channel-link-modal.tsx @@ -112,7 +112,7 @@ export function AddChannelLinkModal({ const discordChannelsResult = useAtomValue( HazelApiClient.query("integration-resources", "getDiscordGuildChannels", { - path: { orgId: organizationId, guildId: externalWorkspaceId }, + params: { orgId: organizationId, guildId: externalWorkspaceId }, }), ) diff --git a/apps/web/src/components/chat-sync/add-connection-modal.tsx b/apps/web/src/components/chat-sync/add-connection-modal.tsx index 8a2aef665..112d245c2 100644 --- a/apps/web/src/components/chat-sync/add-connection-modal.tsx +++ b/apps/web/src/components/chat-sync/add-connection-modal.tsx @@ -48,7 +48,7 @@ export function AddConnectionModal({ const guildsResult = useAtomValue( HazelApiClient.query("integration-resources", "getDiscordGuilds", { - path: { orgId: organizationId }, + params: { orgId: organizationId }, }), ) diff --git a/apps/web/src/components/chat/channel-join-banner.tsx b/apps/web/src/components/chat/channel-join-banner.tsx index c0b0e7842..8338f5466 100644 --- a/apps/web/src/components/chat/channel-join-banner.tsx +++ b/apps/web/src/components/chat/channel-join-banner.tsx @@ -1,6 +1,6 @@ import { useAtomSet } from "@effect/atom-react" import type { ChannelId } from "@hazel/schema" -import { UserId } from "@hazel/schema" +import type { UserId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import IconHashtag from "~/components/icons/icon-hashtag" import { Button } from "~/components/ui/button" @@ -33,7 +33,7 @@ export function ChannelJoinBanner({ channelId }: ChannelJoinBannerProps) { const exit = await joinChannel({ channelId, - userId: UserId.make(user.id), + userId: user.id as UserId, }) exitToast(exit) diff --git a/apps/web/src/components/chat/message-reply-section.tsx b/apps/web/src/components/chat/message-reply-section.tsx index a89d44020..fd3d6799c 100644 --- a/apps/web/src/components/chat/message-reply-section.tsx +++ b/apps/web/src/components/chat/message-reply-section.tsx @@ -45,7 +45,7 @@ export function MessageReplySection({ replyToMessageId, onClick }: MessageReplyS className="flex w-fit items-center gap-1 pl-12 text-left hover:bg-transparent" onClick={onClick} > - {Result.builder(messageResult) + {AsyncResult.builder(messageResult) .onInitial(() => ( <>
diff --git a/apps/web/src/components/chat/slate-editor/slate-message-editor.tsx b/apps/web/src/components/chat/slate-editor/slate-message-editor.tsx index cda9dc7cd..19ae4b504 100644 --- a/apps/web/src/components/chat/slate-editor/slate-message-editor.tsx +++ b/apps/web/src/components/chat/slate-editor/slate-message-editor.tsx @@ -466,7 +466,7 @@ export const SlateMessageEditor = forwardRef r._tag === "Fail") + const error = failReason ? (failReason as any).error : null let message = "Command failed" if (error && typeof error === "object" && "_tag" in error) { diff --git a/apps/web/src/components/gif-picker/use-klipy.ts b/apps/web/src/components/gif-picker/use-klipy.ts index c8599187a..902072764 100644 --- a/apps/web/src/components/gif-picker/use-klipy.ts +++ b/apps/web/src/components/gif-picker/use-klipy.ts @@ -23,7 +23,7 @@ interface UseKlipyReturn { export function useKlipy({ perPage = 25 }: UseKlipyOptions = {}): UseKlipyReturn { // === Auto-fetching query atoms (no useEffect needed) === const trendingResult = useAtomValue( - HazelApiClient.query("klipy", "trending", { urlParams: { page: 1, per_page: perPage } }), + HazelApiClient.query("klipy", "trending", { query: { page: 1, per_page: perPage } }), ) const categoriesResult = useAtomValue(HazelApiClient.query("klipy", "categories", {})) @@ -56,14 +56,14 @@ export function useKlipy({ perPage = 25 }: UseKlipyOptions = {}): UseKlipyReturn const trendingInitRef = useRef(false) if (!trendingInitRef.current && AsyncResult.isSuccess(trendingResult) && overrideGifs === null) { trendingInitRef.current = true - pageRef.current = trendingAsyncResult.value.current_page - const more = trendingAsyncResult.value.has_next + pageRef.current = trendingResult.value.current_page + const more = trendingResult.value.has_next hasMoreRef.current = more setHasMore(more) } // === Derived display values === - const categories = AsyncResult.isSuccess(categoriesResult) ? [...categoriesAsyncResult.value.categories] : [] + const categories = AsyncResult.isSuccess(categoriesResult) ? [...categoriesResult.value.categories] : [] let gifs: KlipyGif[] let isLoading: boolean @@ -72,7 +72,7 @@ export function useKlipy({ perPage = 25 }: UseKlipyOptions = {}): UseKlipyReturn gifs = overrideGifs isLoading = isMutating } else if (AsyncResult.isSuccess(trendingResult)) { - gifs = [...trendingAsyncResult.value.data] + gifs = [...trendingResult.value.data] isLoading = false } else { gifs = [] @@ -93,9 +93,9 @@ export function useKlipy({ perPage = 25 }: UseKlipyOptions = {}): UseKlipyReturn try { let exit: Exit.Exit if (query) { - exit = await searchRef.current({ urlParams: { q: query, page, per_page: perPage } }) + exit = await searchRef.current({ query: { q: query, page, per_page: perPage } }) } else { - exit = await trendingMutRef.current({ urlParams: { page, per_page: perPage } }) + exit = await trendingMutRef.current({ query: { page, per_page: perPage } }) } if (requestIdRef.current !== id) return diff --git a/apps/web/src/components/integrations/add-github-subscription-modal.tsx b/apps/web/src/components/integrations/add-github-subscription-modal.tsx index c9d75b58b..7f1074354 100644 --- a/apps/web/src/components/integrations/add-github-subscription-modal.tsx +++ b/apps/web/src/components/integrations/add-github-subscription-modal.tsx @@ -76,7 +76,7 @@ export function AddGitHubSubscriptionModal({ // Fetch repositories const repositoriesResult = useAtomValue( HazelApiClient.query("integration-resources", "getGitHubRepositories", { - path: { orgId: organizationId }, + params: { orgId: organizationId }, urlParams: { page: 1, perPage: 100 }, }), ) @@ -141,7 +141,7 @@ export function AddGitHubSubscriptionModal({ } // Get repositories from result - const repositories = Result.builder(repositoriesResult) + const repositories = AsyncResult.builder(repositoriesResult) .onSuccess((data) => data?.repositories ?? []) .orElse(() => []) @@ -226,7 +226,7 @@ export function AddGitHubSubscriptionModal({ {selectedChannel && (
- {Result.builder(repositoriesResult) + {AsyncResult.builder(repositoriesResult) .onInitial(() => (
diff --git a/apps/web/src/components/integrations/github-pr-embed.tsx b/apps/web/src/components/integrations/github-pr-embed.tsx index 9d26a10d3..294dde1e1 100644 --- a/apps/web/src/components/integrations/github-pr-embed.tsx +++ b/apps/web/src/components/integrations/github-pr-embed.tsx @@ -221,13 +221,13 @@ export function GitHubPREmbed({ url, orgId }: GitHubPREmbedProps) { const resourceResult = useAtomValue( HazelApiClient.query("integration-resources", "fetchGitHubPR", { - path: { orgId }, + params: { orgId }, urlParams: { url }, timeToLive: "3 minutes", }), ) - return Result.builder(resourceResult) + return AsyncResult.builder(resourceResult) .onInitial(() => ) .onErrorTag("IntegrationNotConnectedForPreviewError", () => ( ) .onErrorTag("IntegrationNotConnectedForPreviewError", () => ( "system" as const, }) @@ -52,7 +52,7 @@ export const themeAtom = Atom.kvs({ export const brandColorAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "brand-color", - schema: Schema.NullOr(HexColorSchema), + schema: Schema.toCodecIso(Schema.NullOr(HexColorSchema)), defaultValue: () => DEFAULT_BRAND_COLOR as string, }) @@ -60,7 +60,7 @@ export const brandColorAtom = Atom.kvs({ export const themeCustomizationAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "hazel-theme-customization", - schema: Schema.NullOr(ThemeCustomizationSchema), + schema: Schema.toCodecIso(Schema.NullOr(ThemeCustomizationSchema)), defaultValue: () => getDefaultThemeCustomization() as ThemeModel.ThemeCustomization, }) diff --git a/apps/web/src/db/actions.ts b/apps/web/src/db/actions.ts index dbc519f59..e9f2485fd 100644 --- a/apps/web/src/db/actions.ts +++ b/apps/web/src/db/actions.ts @@ -1,7 +1,7 @@ import { type User } from "@hazel/domain/models" -import { - type AttachmentId, - type ChannelIcon, +import type { + AttachmentId, + ChannelIcon, ChannelId, ChannelMemberId, ChannelSectionId, @@ -10,7 +10,7 @@ import { MessageReactionId, OrganizationId, PinnedMessageId, - type UserId, + UserId, } from "@hazel/schema" import { Cause, Effect, Schedule, Duration } from "effect" import { isRetryableError } from "~/lib/error-messages" @@ -76,7 +76,7 @@ export const sendMessageAction = optimisticAction({ attachmentIds?: AttachmentId[] onRetryAttempt?: (attempt: number) => void }) => { - const messageId = props.messageId ?? MessageId.make(crypto.randomUUID()) + const messageId = props.messageId ?? crypto.randomUUID() as MessageId messageCollection.insert({ id: messageId, @@ -161,7 +161,7 @@ export const createChannelAction = optimisticAction({ currentUserId: UserId addAllMembers?: boolean }) => { - const channelId = ChannelId.make(crypto.randomUUID()) + const channelId = crypto.randomUUID() as ChannelId const now = new Date() // Optimistically insert the channel @@ -180,7 +180,7 @@ export const createChannelAction = optimisticAction({ // Add creator as member channelMemberCollection.insert({ - id: ChannelMemberId.make(crypto.randomUUID()), + id: crypto.randomUUID() as ChannelMemberId, channelId: channelId, userId: props.currentUserId, isHidden: false, @@ -227,7 +227,7 @@ export const createDmChannelAction = optimisticAction({ name?: string currentUserId: UserId }) => { - const channelId = ChannelId.make(crypto.randomUUID()) + const channelId = crypto.randomUUID() as ChannelId const now = new Date() let channelName = props.name @@ -251,7 +251,7 @@ export const createDmChannelAction = optimisticAction({ // Add current user as member channelMemberCollection.insert({ - id: ChannelMemberId.make(crypto.randomUUID()), + id: crypto.randomUUID() as ChannelMemberId, channelId: channelId, userId: props.currentUserId, isHidden: false, @@ -267,7 +267,7 @@ export const createDmChannelAction = optimisticAction({ // Add all participants as members for (const participantId of props.participantIds) { channelMemberCollection.insert({ - id: ChannelMemberId.make(crypto.randomUUID()), + id: crypto.randomUUID() as ChannelMemberId, channelId: channelId, userId: participantId, isHidden: false, @@ -308,7 +308,7 @@ export const createOrganizationAction = optimisticAction({ runtime: runtime, onMutate: (props: { name: string; slug: string; logoUrl?: string | null }) => { - const organizationId = OrganizationId.make(crypto.randomUUID()) + const organizationId = crypto.randomUUID() as OrganizationId const now = new Date() // Optimistically insert the organization @@ -369,7 +369,7 @@ export const toggleReactionAction = optimisticAction({ } // Toggle on: insert a new reaction - const reactionId = MessageReactionId.make(crypto.randomUUID()) + const reactionId = crypto.randomUUID() as MessageReactionId messageReactionCollection.insert({ id: reactionId, messageId: props.messageId, @@ -415,7 +415,7 @@ export const createThreadAction = optimisticAction({ organizationId: OrganizationId currentUserId: UserId }) => { - const threadChannelId = props.threadChannelId ?? ChannelId.make(crypto.randomUUID()) + const threadChannelId = props.threadChannelId ?? crypto.randomUUID() as ChannelId const now = new Date() // Create thread channel @@ -434,7 +434,7 @@ export const createThreadAction = optimisticAction({ // Add creator as member channelMemberCollection.insert({ - id: ChannelMemberId.make(crypto.randomUUID()), + id: crypto.randomUUID() as ChannelMemberId, channelId: threadChannelId, userId: props.currentUserId, isHidden: false, @@ -561,7 +561,7 @@ export const pinMessageAction = optimisticAction({ runtime: runtime, onMutate: (props: { messageId: MessageId; channelId: ChannelId; userId: UserId }) => { - const pinnedMessageId = PinnedMessageId.make(crypto.randomUUID()) + const pinnedMessageId = crypto.randomUUID() as PinnedMessageId pinnedMessageCollection.insert({ id: pinnedMessageId, channelId: props.channelId, @@ -644,7 +644,7 @@ export const joinChannelAction = optimisticAction({ runtime: runtime, onMutate: (props: { channelId: ChannelId; userId: UserId }) => { - const memberId = ChannelMemberId.make(crypto.randomUUID()) + const memberId = crypto.randomUUID() as ChannelMemberId const now = new Date() channelMemberCollection.insert({ id: memberId, @@ -710,7 +710,7 @@ export const createChannelSectionAction = optimisticAction({ runtime: runtime, onMutate: (props: { organizationId: OrganizationId; name: string; order?: number }) => { - const sectionId = ChannelSectionId.make(crypto.randomUUID()) + const sectionId = crypto.randomUUID() as ChannelSectionId const now = new Date() channelSectionCollection.insert({ @@ -900,7 +900,7 @@ export const createCustomEmojiAction = optimisticAction({ imageUrl: string createdBy: UserId }) => { - const emojiId = CustomEmojiId.make(crypto.randomUUID()) + const emojiId = crypto.randomUUID() as CustomEmojiId const now = new Date() customEmojiCollection.insert({ diff --git a/apps/web/src/lib/auth-token.ts b/apps/web/src/lib/auth-token.ts index f0e3bd3be..f55cc5b0c 100644 --- a/apps/web/src/lib/auth-token.ts +++ b/apps/web/src/lib/auth-token.ts @@ -26,8 +26,8 @@ const BASE_BACKOFF_MS = 1000 // 1s, 2s, 4s // Shared Refs (prevents concurrent refreshes across all callers) // ============================================================================ -const isRefreshingRef = Ref.unsafeMake(false) -const refreshDeferredRef = Ref.unsafeMake | null>(null) +const isRefreshingRef = Effect.runSync(Ref.make(false)) +const refreshDeferredRef = Effect.runSync(Ref.make | null>(null)) // ============================================================================ // Platform-specific layers @@ -76,37 +76,37 @@ const platformTag = () => (isTauri() ? "desktop" : "web") /** Read access token from the correct platform storage */ const readAccessToken = Effect.fn("readAccessToken")(function* () { if (isTauri()) { - return Option.getOrNull(yield* TokenStorage.getAccessToken.pipe(Effect.provide(desktopStorageLive))) + const storage = yield* TokenStorage + return Option.getOrNull(yield* storage.getAccessToken) } - return Option.getOrNull(yield* WebTokenStorage.getAccessToken.pipe(Effect.provide(webStorageLive))) -}) + const storage = yield* WebTokenStorage + return Option.getOrNull(yield* storage.getAccessToken) +}).pipe(Effect.provide(isTauri() ? desktopStorageLive : webStorageLive)) as Effect.Effect /** Read refresh token from the correct platform storage */ const readRefreshToken = Effect.fn("readRefreshToken")(function* () { if (isTauri()) { - return yield* TokenStorage.getRefreshToken.pipe(Effect.provide(desktopStorageLive)) + const storage = yield* TokenStorage + return yield* storage.getRefreshToken } - return yield* WebTokenStorage.getRefreshToken.pipe(Effect.provide(webStorageLive)) -}) + const storage = yield* WebTokenStorage + return yield* storage.getRefreshToken +}).pipe(Effect.provide(isTauri() ? desktopStorageLive : webStorageLive)) as Effect.Effect> /** Store tokens in the correct platform storage */ -const storeTokens = Effect.fn("storeTokens")(function* ( - accessToken: string, - refreshToken: string, - expiresIn: number, -) { - if (isTauri()) { - yield* TokenStorage.storeTokens(accessToken, refreshToken, expiresIn).pipe( - Effect.provide(desktopStorageLive), - Effect.orDie, - ) - } else { - yield* WebTokenStorage.storeTokens(accessToken, refreshToken, expiresIn).pipe( - Effect.provide(webStorageLive), - Effect.orDie, - ) - } -}) +const storeTokens = (accessToken: string, refreshToken: string, expiresIn: number) => + Effect.gen(function* () { + if (isTauri()) { + const storage = yield* TokenStorage + yield* storage.storeTokens(accessToken, refreshToken, expiresIn) + } else { + const storage = yield* WebTokenStorage + yield* storage.storeTokens(accessToken, refreshToken, expiresIn) + } + }).pipe( + Effect.provide(isTauri() ? desktopStorageLive : webStorageLive), + Effect.orDie, + ) // ============================================================================ // Core Effects @@ -116,7 +116,7 @@ const storeTokens = Effect.fn("storeTokens")(function* ( * Get the current access token from the appropriate platform storage. * Returns null if not authenticated. */ -const getAccessTokenEffect: Effect.Effect = readAccessToken().pipe( +const getAccessTokenEffect: Effect.Effect = readAccessToken.pipe( Effect.catch(() => Effect.succeed(null)), Effect.withSpan("getAccessToken"), ) @@ -156,7 +156,7 @@ const forceRefreshEffect: Effect.Effect = Effect.gen(function* () { } // Get refresh token to check if we can refresh - const refreshTokenOpt = yield* readRefreshToken().pipe( + const refreshTokenOpt = yield* readRefreshToken.pipe( Effect.catch(() => Effect.succeed(Option.none())), ) diff --git a/apps/web/src/lib/error-messages.ts b/apps/web/src/lib/error-messages.ts index 35d9e822b..84c4324eb 100644 --- a/apps/web/src/lib/error-messages.ts +++ b/apps/web/src/lib/error-messages.ts @@ -1,5 +1,6 @@ -import type { HttpClientError } from "@effect/platform/HttpClientError" -import { RpcClientError } from "@effect/rpc/RpcClientError" +import { Schema } from "effect" +import type { HttpClientError } from "effect/unstable/http" +import { RpcClientError } from "effect/unstable/rpc" import { AIProviderUnavailableError, AIRateLimitError, @@ -123,7 +124,7 @@ export const CommonAppErrorSchema = Schema.Union([ export type CommonAppError = | typeof CommonAppErrorSchema.Type // Non-Schema errors (still have _tag but not Schema.TaggedError) - | ParseError + | Schema.SchemaError | HttpClientError /** diff --git a/apps/web/src/lib/platform-storage/platform-key-value-store.ts b/apps/web/src/lib/platform-storage/platform-key-value-store.ts index d2f2185c5..03c0c5902 100644 --- a/apps/web/src/lib/platform-storage/platform-key-value-store.ts +++ b/apps/web/src/lib/platform-storage/platform-key-value-store.ts @@ -7,7 +7,7 @@ */ import { BrowserKeyValueStore } from "@effect/platform-browser" -import type * as KeyValueStore from "@effect/platform/KeyValueStore" +import type * as KeyValueStore from "effect/unstable/persistence/KeyValueStore" import type { Layer } from "effect" import { isTauri } from "~/lib/tauri" import { layerTauriStore } from "./tauri-key-value-store" diff --git a/apps/web/src/lib/platform-storage/tauri-key-value-store.ts b/apps/web/src/lib/platform-storage/tauri-key-value-store.ts index 195386ec3..f89cd993d 100644 --- a/apps/web/src/lib/platform-storage/tauri-key-value-store.ts +++ b/apps/web/src/lib/platform-storage/tauri-key-value-store.ts @@ -7,8 +7,8 @@ * Store file: settings.json (separate from auth.json used for tokens) */ -import * as KeyValueStore from "@effect/platform/KeyValueStore" -import { SystemError } from "@effect/platform/Error" +import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore" +import { KeyValueStoreError as SystemError } from "effect/unstable/persistence/KeyValueStore" import { getTauriStore, type TauriStoreApi } from "@hazel/desktop/bridge" import { Effect, Layer, Option } from "effect" diff --git a/apps/web/src/lib/services/common/api-client.ts b/apps/web/src/lib/services/common/api-client.ts index e789d4fe1..ee9502535 100644 --- a/apps/web/src/lib/services/common/api-client.ts +++ b/apps/web/src/lib/services/common/api-client.ts @@ -4,9 +4,8 @@ * @description HTTP client that uses Bearer tokens for desktop and cookies for web */ -import * as FetchHttpClient from "@effect/platform/FetchHttpClient" -import * as HttpApiClient from "@effect/platform/HttpApiClient" -import * as HttpClient from "@effect/platform/HttpClient" +import { FetchHttpClient, HttpClient } from "effect/unstable/http" +import { HttpApiClient } from "effect/unstable/httpapi" import { HazelApi } from "@hazel/domain/http" import { ServiceMap, Layer } from "effect" import * as Effect from "effect/Effect" diff --git a/apps/web/src/lib/services/common/network-mode.ts b/apps/web/src/lib/services/common/network-mode.ts index e34f3ce6e..f68e3eb88 100644 --- a/apps/web/src/lib/services/common/network-mode.ts +++ b/apps/web/src/lib/services/common/network-mode.ts @@ -1,5 +1,6 @@ import * as Chunk from "effect/Chunk" import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" import * as ServiceMap from "effect/ServiceMap" import * as Stream from "effect/Stream" import * as SubscriptionRef from "effect/SubscriptionRef" diff --git a/apps/web/src/providers/chat-provider.tsx b/apps/web/src/providers/chat-provider.tsx index 7469d1543..90da4a825 100644 --- a/apps/web/src/providers/chat-provider.tsx +++ b/apps/web/src/providers/chat-provider.tsx @@ -1,13 +1,13 @@ import { AsyncResult } from "effect/unstable/reactivity" import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { Channel } from "@hazel/domain/models" -import { - type AttachmentId, +import type { + AttachmentId, ChannelId, - type MessageId, - type MessageReactionId, - type OrganizationId, - type PinnedMessageId, + MessageId, + MessageReactionId, + OrganizationId, + PinnedMessageId, UserId, } from "@hazel/schema" import { Exit } from "effect" @@ -396,7 +396,7 @@ export function ChatProvider({ channelId, organizationId, children, onMessageSen const tx = await sendMessageMutation({ channelId, - authorId: UserId.make(user.id), + authorId: user.id as UserId, content, replyToMessageId: savedReplyToMessageId, threadChannelId: null, @@ -503,7 +503,7 @@ export function ChatProvider({ channelId, organizationId, children, onMessageSen messageId, channelId, emoji, - userId: UserId.make(user.id), + userId: user.id as UserId, }) exitToast(tx) @@ -529,7 +529,7 @@ export function ChatProvider({ channelId, organizationId, children, onMessageSen const exit = await pinMessageMutation({ messageId, channelId, - userId: UserId.make(user.id), + userId: user.id as UserId, }) exitToast(exit) @@ -576,7 +576,7 @@ export function ChatProvider({ channelId, organizationId, children, onMessageSen if (!user?.id) return // Generate thread channel ID upfront for optimistic UI - const threadChannelId = ChannelId.make(crypto.randomUUID()) + const threadChannelId = crypto.randomUUID() as ChannelId // Open panel IMMEDIATELY with optimistic ID setActiveThreadChannelId(threadChannelId) @@ -591,7 +591,7 @@ export function ChatProvider({ channelId, organizationId, children, onMessageSen messageId, parentChannelId: channelId, organizationId, - currentUserId: UserId.make(user.id), + currentUserId: user.id as UserId, }) // Clear pending state diff --git a/apps/web/src/routes/_app/$orgSlug/my-settings/linked-accounts.tsx b/apps/web/src/routes/_app/$orgSlug/my-settings/linked-accounts.tsx index e3dcb5bcb..76690e87b 100644 --- a/apps/web/src/routes/_app/$orgSlug/my-settings/linked-accounts.tsx +++ b/apps/web/src/routes/_app/$orgSlug/my-settings/linked-accounts.tsx @@ -68,7 +68,7 @@ function LinkedAccountsSettings() { if (!user?.organizationId) return setIsConnectingDiscord(true) const result = await getOAuthUrlMutation({ - path: { orgId: user.organizationId, provider: "discord" }, + params: { orgId: user.organizationId, provider: "discord" }, urlParams: { level: "user" }, }) @@ -85,7 +85,7 @@ function LinkedAccountsSettings() { if (!user?.organizationId) return setIsDisconnectingDiscord(true) const result = await disconnectMutation({ - path: { orgId: user.organizationId, provider: "discord" }, + params: { orgId: user.organizationId, provider: "discord" }, urlParams: { level: "user" }, }) diff --git a/apps/web/src/routes/_app/$orgSlug/settings/authentication.tsx b/apps/web/src/routes/_app/$orgSlug/settings/authentication.tsx index 814883c31..f480d75e7 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/authentication.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/authentication.tsx @@ -195,12 +195,12 @@ function DomainManagement({ const removeDomainResult = useAtomValue(removeOrganizationDomainMutation) const removeDomain = useAtomSet(removeOrganizationDomainMutation, { mode: "promiseExit" }) - const isAddingDomain = addDomainAsyncResult.waiting - const isRemovingDomain = removeDomainAsyncResult.waiting + const isAddingDomain = addDomainResult.waiting + const isRemovingDomain = removeDomainResult.waiting const domains: Domain[] = AsyncResult.getOrElse(domainsResult, () => []) as Domain[] // Only show loading on initial load, not during background refreshes (polling) - const isLoadingDomains = domainsAsyncResult._tag === "Initial" + const isLoadingDomains = domainsResult._tag === "Initial" const reactivityKeys = [`organizationDomains:${organizationId}`] as const @@ -291,7 +291,7 @@ function AuthenticationSettings() { mode: "promiseExit", }) - const isLoadingPortalLink = getAdminPortalLinkAsyncResult.waiting + const isLoadingPortalLink = getAdminPortalLinkResult.waiting const handleOpenPortal = async (intent: "sso" | "domain_verification") => { if (!organizationId) return diff --git a/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx b/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx index 0e0037b60..b136d2858 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx @@ -157,7 +157,7 @@ function IntegrationConfigPage() { // Make authenticated API call to get OAuth URL, then redirect // This ensures the bearer token auth is properly sent const exit = await getOAuthUrlMutation({ - path: { orgId: organizationId, provider: integrationId as IntegrationProvider }, + params: { orgId: organizationId, provider: integrationId as IntegrationProvider }, urlParams: { level: "organization" }, }) @@ -176,7 +176,7 @@ function IntegrationConfigPage() { if (!organizationId) return setIsDisconnecting(true) const exit = await disconnectMutation({ - path: { orgId: organizationId, provider: integrationId as IntegrationProvider }, + params: { orgId: organizationId, provider: integrationId as IntegrationProvider }, urlParams: { level: "organization" }, }) @@ -201,7 +201,7 @@ function IntegrationConfigPage() { setIsConnecting(true) const exit = await connectApiKeyMutation({ - path: { orgId: organizationId, provider: integrationId as IntegrationProvider }, + params: { orgId: organizationId, provider: integrationId as IntegrationProvider }, payload: { token, baseUrl }, }) @@ -763,7 +763,7 @@ function GitHubRepositoryAccessSection({ organizationId }: { organizationId: Org const repositoriesResult = useAtomValue( HazelApiClient.query("integration-resources", "getGitHubRepositories", { - path: { orgId: organizationId }, + params: { orgId: organizationId }, urlParams: { page, perPage }, }), ) diff --git a/apps/web/src/routes/join/$slug.tsx b/apps/web/src/routes/join/$slug.tsx index f85b38e25..e6bcd77ed 100644 --- a/apps/web/src/routes/join/$slug.tsx +++ b/apps/web/src/routes/join/$slug.tsx @@ -68,7 +68,7 @@ function JoinPage() { const orgResult = useAtomValue(getOrgBySlugPublicQuery(slug)) const joinOrg = useAtomSet(joinViaPublicInviteMutation, { mode: "promiseExit" }) - const isLoading = orgAsyncResult._tag === "Initial" || orgAsyncResult.waiting + const isLoading = orgResult._tag === "Initial" || orgResult.waiting const handleSignIn = () => { login({ diff --git a/bun.lock b/bun.lock index b6df7934d..9ef4dfadd 100644 --- a/bun.lock +++ b/bun.lock @@ -593,15 +593,15 @@ }, "catalogs": { "effect": { - "@effect/ai-openrouter": "4.0.0-beta.32", - "@effect/atom-react": "4.0.0-beta.32", - "@effect/opentelemetry": "4.0.0-beta.32", - "@effect/platform-browser": "4.0.0-beta.32", - "@effect/platform-bun": "4.0.0-beta.32", - "@effect/platform-node": "4.0.0-beta.32", - "@effect/sql-pg": "4.0.0-beta.32", - "@effect/vitest": "4.0.0-beta.32", - "effect": "4.0.0-beta.32", + "@effect/ai-openrouter": "4.0.0-beta.33", + "@effect/atom-react": "4.0.0-beta.33", + "@effect/opentelemetry": "4.0.0-beta.33", + "@effect/platform-browser": "4.0.0-beta.33", + "@effect/platform-bun": "4.0.0-beta.33", + "@effect/platform-node": "4.0.0-beta.33", + "@effect/sql-pg": "4.0.0-beta.33", + "@effect/vitest": "4.0.0-beta.33", + "effect": "4.0.0-beta.33", }, }, "packages": { @@ -895,27 +895,27 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - "@effect/ai-openrouter": ["@effect/ai-openrouter@4.0.0-beta.32", "", { "peerDependencies": { "effect": "^4.0.0-beta.32" } }, "sha512-WSK9S40cq50U8I0RJTHjRMubV/WevdleSc7XF9OJXMVaxrUgN/2oLTMEQOopnb/Gk/0bUtjLYxw7XKJ5zy7rDQ=="], + "@effect/ai-openrouter": ["@effect/ai-openrouter@4.0.0-beta.33", "", { "peerDependencies": { "effect": "^4.0.0-beta.33" } }, "sha512-J+ixuKp8Ya7RkPEL7d79ySQmjsbCR/1DLvDbwLv1OIuegtOfN7Abo16L6Qa19Vo/JV67Uwds9uXmchsE+txrAg=="], - "@effect/atom-react": ["@effect/atom-react@4.0.0-beta.32", "", { "peerDependencies": { "effect": "^4.0.0-beta.32", "react": "^19.2.4", "scheduler": "*" } }, "sha512-xft2Fn8RRI6sccqM6iCq1EmYT9K9e/q4rpNJCEpj7rEljZZBwkJBoet7R64OrDZFyT9COys+8M07pegLS3q69g=="], + "@effect/atom-react": ["@effect/atom-react@4.0.0-beta.33", "", { "peerDependencies": { "effect": "^4.0.0-beta.33", "react": "^19.2.4", "scheduler": "*" } }, "sha512-w4sbCoBJFez5BpD/fM4pYt9xGGKaPkMnSarcgQRjhmrUU6bsDu82jaYGGduIInucYVc9EanXsMcTDCwtU43X0Q=="], - "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.32", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.32" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-NsR7vJBXDbYr+8fcAaKUyGC022s7lTeUODPD8kabqOlVrs86k/LNcTtVtGXUAjY4o1u/c3kipJ0p4z9rCniwsQ=="], + "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.33", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.33" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-Vi4G/ZRdEv4HMww+VAof2asOvoayO6qtO9dmwLK61gYagVgK20a+k3n28NCSg6zsHkrx7hlljLT+nW0gI2usZg=="], "@effect/platform": ["@effect/platform@0.94.5", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.17" } }, "sha512-z05APUiDDPbodhTkH/RJqOLoCU11bU2IZLfcwLFrld03+ob1VeqRnELQlmueLIYm6NZifHAtjl32V+GRt34y4A=="], - "@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.32", "", { "dependencies": { "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^4.0.0-beta.32" } }, "sha512-sV5d/Qs6y74DOcIQe4QaCrNhqVwRvZfi13cQAMmnv7xX+El7TDn0EZ0i8xmT4mCF2KrG3mHKHCwKoDOIHgEcxg=="], + "@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.33", "", { "dependencies": { "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^4.0.0-beta.33" } }, "sha512-8DO/bLu04edHKW7GffRNcnBi8Z4r4F4sXR/VIPZfVA8AceeZ2BEJJftSceozftnlaxm8MsqbcTz4cSPnyuDVYw=="], - "@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.32", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.32" }, "peerDependencies": { "effect": "^4.0.0-beta.32" } }, "sha512-/za6Ps9sh6S3or9OxfR1LIxseVuw/3E4qbtcs6s4nMFzrLyh8bGxp4rqqUnQ1pZxBV8i/eziHpg9M8xOghlLkQ=="], + "@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.33", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.33" }, "peerDependencies": { "effect": "^4.0.0-beta.33" } }, "sha512-CtjRdSC9ZFGREw0PYL5Y1bGVo3pOZ3ZkwtO9aWU699Tq6I+/o4HJLfKKLfo2G17BAkEoq0Gn6hyoQqFxUcplWg=="], - "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.32", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.32", "mime": "^4.1.0", "undici": "^7.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.32", "ioredis": "^5.7.0" } }, "sha512-rgMQbbevkoQsrvVvbEe9qSU2woMXYSDLlO8aZEZ0GgXROEZC7r2tO5JL+BANKSCvauE916uYrSxwMovLBcRbMg=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.33", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.33", "mime": "^4.1.0", "undici": "^7.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.33", "ioredis": "^5.7.0" } }, "sha512-mw/zCuq4bSRP5nm3hPlfjX+veKlG6kC3NleuMhRuVSa8NzlHF08rXptd6S9ks9JuDz5F6dgzIf/beaGAYF8TmA=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.32", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.32" } }, "sha512-gpEV2uPom22DbqobiBK75em7Jo6wW4sti7YA2vD1IkZPmOpC7snpB50X7CgeM9NlRkbaqC7xbKztoNk+/BBJkA=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.33", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.33" } }, "sha512-jaJnvYz1IiPZyN//fCJsvwnmujJS5KD8noCVVLhb4ZGCWKhQpt0x2iuax6HFzMlPEQSfl04GLU+PVKh0nkzPyA=="], "@effect/rpc": ["@effect/rpc@0.73.2", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.94.5", "effect": "^3.19.18" } }, "sha512-td7LHDgBOYKg+VgGWEelD8rSAmvjXz7am17vfxZROX5qIYuvH7drL/z4p5xQFadhHZ7DYdlFpqdO9ggc77OCIw=="], - "@effect/sql-pg": ["@effect/sql-pg@4.0.0-beta.32", "", { "dependencies": { "pg": "^8.18.0", "pg-connection-string": "2.11.0", "pg-cursor": "^2.17.0", "pg-pool": "^3.11.0", "pg-types": "^4.1.0" }, "peerDependencies": { "effect": "^4.0.0-beta.32" } }, "sha512-GGL+uc3vouBnrHCOvz2M8OE9FevG0D2W2kUDsHL0uv1rDdAXAYpP41WKb1+hZNfydN0WVQs71Jxdtlaq3ZDSAQ=="], + "@effect/sql-pg": ["@effect/sql-pg@4.0.0-beta.33", "", { "dependencies": { "pg": "^8.18.0", "pg-connection-string": "2.11.0", "pg-cursor": "^2.17.0", "pg-pool": "^3.11.0", "pg-types": "^4.1.0" }, "peerDependencies": { "effect": "^4.0.0-beta.33" } }, "sha512-WTUre9mS73PY2a36PIS55VhEwL6FOCfiRj2R7oVr5/eZGjiBCrs9xluK6X/so74elnfvtPIbHmAEkEldUukEuA=="], - "@effect/vitest": ["@effect/vitest@4.0.0-beta.32", "", { "peerDependencies": { "effect": "^4.0.0-beta.32", "vitest": "^3.0.0 || ^4.0.0" } }, "sha512-ZXzvFtS2skcJx8vL/pkBvFt2Yxd/FScP3r6EGkCoI94jL9ddd4rcnt8JwhyBQQLiW26+aj5gDnl+gCotjBrruA=="], + "@effect/vitest": ["@effect/vitest@4.0.0-beta.33", "", { "peerDependencies": { "effect": "^4.0.0-beta.33", "vitest": "^3.0.0 || ^4.0.0" } }, "sha512-atoJmncSbrKm8Fb1W+09mju6LwWRdhfvBicHpChwoPWCiij5fFrwRD7EBgIAmYUjycikR2/RYsPpeKXi8L26kw=="], "@electric-sql/client": ["@electric-sql/client@1.5.12", "", { "dependencies": { "@microsoft/fetch-event-source": "^2.0.1" }, "optionalDependencies": { "@rollup/rollup-darwin-arm64": "^4.18.1" }, "bin": { "intent": "bin/intent.mjs" } }, "sha512-mWDEpKog0Zo4WOjReW4x9ELaHsjTthpziLVJkjmSdh/4Y/ZHw5EoY5e9dJX9itPSKVk9GNFz3YfHFv5EAyCOmw=="], @@ -2613,7 +2613,7 @@ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - "effect": ["effect@4.0.0-beta.32", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-22DEm3JB9njSJzYxBXA6h38JHIxLu6JpKQmhQw2C28gwptpnQXc4Ba3bIQubqsKSzYqNjp5fz9YdOZuKSwxkXQ=="], + "effect": ["effect@4.0.0-beta.33", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-ln9emWPd1SemokSdOV43r2CbH1j8GTe9qbPvttmh9/j2OR0WNmj7UpjbN34llQgF9QV4IdcN6QdV2w8G7B7RyQ=="], "effect-rpc-tanstack-devtools": ["effect-rpc-tanstack-devtools@0.1.1", "", { "peerDependencies": { "@effect/rpc": ">=0.70.0", "@tanstack/devtools-event-client": ">=0.3.0", "effect": ">=3.0.0", "react": ">=18.0.0" } }, "sha512-iYddWCEwdUCquNrXexHfOzKPxLR5fD5HalSTgRnr7cW3aRPcTAjQ1XGXel0nJBeUoPcSkiEIuvDVsYYySviLyg=="], @@ -4705,6 +4705,8 @@ "cssstyle/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "dfx/effect": ["effect@4.0.0-beta.32", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-22DEm3JB9njSJzYxBXA6h38JHIxLu6JpKQmhQw2C28gwptpnQXc4Ba3bIQubqsKSzYqNjp5fz9YdOZuKSwxkXQ=="], + "docker-modem/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "dockerode/tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], diff --git a/libs/effect-electric-db-collection/src/collection.ts b/libs/effect-electric-db-collection/src/collection.ts index e732f3229..af6511e4c 100644 --- a/libs/effect-electric-db-collection/src/collection.ts +++ b/libs/effect-electric-db-collection/src/collection.ts @@ -429,17 +429,17 @@ export type EffectCollection< * // messageCollection.utils.awaitTxIdEffect is properly typed! * ``` */ -export function createEffectCollection, I, TRuntime>( +export function createEffectCollection, TRuntime>( config: Omit< EffectElectricCollectionConfig, TRuntime>, "schema" > & { - schema: Schema.Schema + schema: Schema.Schema runtime: ManagedRuntime.ManagedRuntime }, ): EffectCollection { // Convert Effect Schema to StandardSchemaV1 internally - const standardSchema = Schema.standardSchemaV1(config.schema) + const standardSchema = Schema.toStandardSchemaV1(config.schema as any) const options = effectElectricCollectionOptions({ ...config, diff --git a/libs/effect-electric-db-collection/src/handlers.ts b/libs/effect-electric-db-collection/src/handlers.ts index d992ad7c0..cf0e5aa2d 100644 --- a/libs/effect-electric-db-collection/src/handlers.ts +++ b/libs/effect-electric-db-collection/src/handlers.ts @@ -6,7 +6,7 @@ import type { UtilsRecord, } from "@tanstack/db" import type { Txid } from "@tanstack/electric-db-collection" -import { Effect, Exit, type ManagedRuntime } from "effect" +import { Cause, Effect, Exit, type ManagedRuntime } from "effect" import { DeleteError, InsertError, MissingTxIdError, UpdateError } from "./errors" import type { EffectDeleteHandler, EffectInsertHandler, EffectUpdateHandler } from "./types" @@ -52,8 +52,9 @@ export function convertInsertHandler< // Handle the Exit type if (Exit.isFailure(exit)) { const cause = exit.cause - if (cause._tag === "Fail") { - throw cause.error + const failReason = cause.reasons.find(Cause.isFailReason) + if (failReason) { + throw failReason.error } throw new InsertError({ message: `Insert operation failed unexpectedly`, @@ -117,8 +118,9 @@ export function convertUpdateHandler< // Handle the Exit type if (Exit.isFailure(exit)) { const cause = exit.cause - if (cause._tag === "Fail") { - throw cause.error + const failReason = cause.reasons.find(Cause.isFailReason) + if (failReason) { + throw failReason.error } throw new UpdateError({ message: `Update operation failed unexpectedly`, @@ -182,8 +184,9 @@ export function convertDeleteHandler< // Handle the Exit type if (Exit.isFailure(exit)) { const cause = exit.cause - if (cause._tag === "Fail") { - throw cause.error + const failReason = cause.reasons.find(Cause.isFailReason) + if (failReason) { + throw failReason.error } throw new DeleteError({ message: `Delete operation failed unexpectedly`, diff --git a/libs/effect-electric-db-collection/src/optimistic-action.ts b/libs/effect-electric-db-collection/src/optimistic-action.ts index 12f1f265d..cb340f12d 100644 --- a/libs/effect-electric-db-collection/src/optimistic-action.ts +++ b/libs/effect-electric-db-collection/src/optimistic-action.ts @@ -1,4 +1,4 @@ -import { Atom, type Result } from "effect" +import { Atom, type AsyncResult } from "effect/unstable/reactivity" import type { Collection, Transaction } from "@tanstack/db" import { createTransaction } from "@tanstack/db" import type { Txid } from "@tanstack/electric-db-collection" @@ -213,7 +213,7 @@ export function optimisticAction< >( config: OptimisticActionConfig, ): Atom.Writable< - Result.Result< + AsyncResult.AsyncResult< OptimisticActionResult, TError | SyncError | OptimisticActionError >, @@ -241,8 +241,9 @@ export function optimisticAction< if (Exit.isFailure(exit)) { const cause = exit.cause - if (cause._tag === "Fail") { - throw cause.error // Typed TError + const failReason = cause.reasons.find(Cause.isFailReason) + if (failReason) { + throw failReason.error // Typed TError } throw new OptimisticActionError({ message: "Mutation failed unexpectedly", @@ -288,8 +289,9 @@ export function optimisticAction< `cause:`, Cause.pretty(cause), ) - if (cause._tag === "Fail") { - throw cause.error // SyncError + const syncFailReason = cause.reasons.find(Cause.isFailReason) + if (syncFailReason) { + throw syncFailReason.error // SyncError } throw new SyncError({ message: "Sync failed unexpectedly", diff --git a/libs/effect-electric-db-collection/src/service.ts b/libs/effect-electric-db-collection/src/service.ts index a40e5faa5..c4fcb0973 100644 --- a/libs/effect-electric-db-collection/src/service.ts +++ b/libs/effect-electric-db-collection/src/service.ts @@ -158,7 +158,7 @@ export function makeElectricCollectionLayer( collection, insert: (data) => - Effect.async((resume) => { + Effect.callback((resume) => { const tx = collection.insert(data) tx.isPersisted.promise .then(() => resume(Effect.void)) @@ -176,7 +176,7 @@ export function makeElectricCollectionLayer( }), update: (keys, updateFn) => - Effect.async((resume) => { + Effect.callback((resume) => { const tx = collection.update(keys as any, updateFn as any) tx.isPersisted.promise .then(() => resume(Effect.void)) @@ -194,7 +194,7 @@ export function makeElectricCollectionLayer( }), delete: (keys) => - Effect.async((resume) => { + Effect.callback((resume) => { const tx = collection.delete(keys as any) tx.isPersisted.promise .then(() => resume(Effect.void)) diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.ts index df5e510fa..db502d166 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.ts @@ -4,7 +4,7 @@ * @since 1.0.0 */ -import { Atom, Result } from "@effect/atom-react" +import { Atom, AsyncResult } from "effect/unstable/reactivity" import { type Collection, type Context, @@ -25,7 +25,7 @@ import type { CollectionStatus, ConditionalQueryFn, QueryFn, QueryOptions } from */ export const makeCollectionAtom = ( collection: Collection & NonSingleResult, -): Atom.Atom, Error>> => { +): Atom.Atom, Error>> => { return Atom.readable((get) => { // Start sync if not already started collection.startSyncImmediate() @@ -36,22 +36,22 @@ export const makeCollectionAtom = value) - get.setSelf(Result.success(newData)) + get.setSelf(AsyncResult.success(newData)) }) // Cleanup on unmount @@ -63,21 +63,21 @@ export const makeCollectionAtom = value) - return Result.success(initialData) + return AsyncResult.success(initialData) }) } @@ -87,7 +87,7 @@ export const makeCollectionAtom = ( collection: Collection & SingleResult, -): Atom.Atom> => { +): Atom.Atom> => { return Atom.readable((get) => { // Start sync if not already started collection.startSyncImmediate() @@ -98,23 +98,23 @@ export const makeSingleCollectionAtom = 0 ? entries[0]![1] : undefined - get.setSelf(Result.success(newData)) + get.setSelf(AsyncResult.success(newData)) }) // Cleanup on unmount @@ -126,22 +126,22 @@ export const makeSingleCollectionAtom = 0 ? entries[0]![1] : undefined - return Result.success(initialData) + return AsyncResult.success(initialData) }) } @@ -152,7 +152,7 @@ export const makeSingleCollectionAtom = ( queryFn: QueryFn, options?: QueryOptions, -): Atom.Atom, Error>> => { +): Atom.Atom, Error>> => { return Atom.readable((get) => { // Create live query collection const collection = createLiveQueryCollection({ @@ -167,17 +167,17 @@ export const makeQuery = ( const status: CollectionStatus = collection.status if (status === "error") { - get.setSelf(Result.fail(new Error("Query failed to load"))) + get.setSelf(AsyncResult.fail(new Error("Query failed to load"))) return } if (status === "loading" || status === "idle") { - get.setSelf(Result.initial(true)) + get.setSelf(AsyncResult.initial(true)) return } if (status === "cleaned-up") { - get.setSelf(Result.fail(new Error("Query collection has been cleaned up"))) + get.setSelf(AsyncResult.fail(new Error("Query collection has been cleaned up"))) return } @@ -185,7 +185,7 @@ export const makeQuery = ( const isSingleResult = (collection as any).config?.singleResult === true const entries = Array.from(collection.entries()).map(([_, value]) => value) const newData = (isSingleResult ? entries[0] : entries) as InferResultType - get.setSelf(Result.success(newData)) + get.setSelf(AsyncResult.success(newData)) }) // Cleanup on unmount @@ -197,15 +197,15 @@ export const makeQuery = ( const status: CollectionStatus = collection.status if (status === "error") { - return Result.fail(new Error("Query failed to load")) + return AsyncResult.fail(new Error("Query failed to load")) } if (status === "loading" || status === "idle") { - return Result.initial(true) + return AsyncResult.initial(true) } if (status === "cleaned-up") { - return Result.fail(new Error("Query collection has been cleaned up")) + return AsyncResult.fail(new Error("Query collection has been cleaned up")) } // Get current data - handle both single and array results @@ -213,7 +213,7 @@ export const makeQuery = ( const entries = Array.from(collection.entries()).map(([_, value]) => value) const initialData = (isSingleResult ? entries[0] : entries) as InferResultType - return Result.success(initialData) + return AsyncResult.success(initialData) }) } @@ -227,7 +227,7 @@ export const makeQueryUnsafe = ( ): Atom.Atom | undefined> => { return Atom.readable((get) => { const result = get(makeQuery(queryFn, options)) - return Result.getOrElse(result, constUndefined) as InferResultType | undefined + return AsyncResult.getOrElse(result, constUndefined) as InferResultType | undefined }) } @@ -238,7 +238,7 @@ export const makeQueryUnsafe = ( export const makeQueryConditional = ( queryFn: ConditionalQueryFn, options?: QueryOptions, -): Atom.Atom, Error> | undefined> => { +): Atom.Atom, Error> | undefined> => { return Atom.readable((get) => { // Create a proxy query builder to detect if query function returns null/undefined // without actually executing any query methods diff --git a/libs/tanstack-db-atom/src/types.ts b/libs/tanstack-db-atom/src/types.ts index ce0e6661b..d3d4d4b9b 100644 --- a/libs/tanstack-db-atom/src/types.ts +++ b/libs/tanstack-db-atom/src/types.ts @@ -3,7 +3,7 @@ * @since 1.0.0 */ -import type { Atom, Result } from "@effect/atom-react" +import type { Atom, AsyncResult } from "effect/unstable/reactivity" import type { Collection, Context, diff --git a/package.json b/package.json index 7b7acdee9..a39aeb447 100644 --- a/package.json +++ b/package.json @@ -10,15 +10,15 @@ ], "catalogs": { "effect": { - "effect": "4.0.0-beta.32", - "@effect/platform-bun": "4.0.0-beta.32", - "@effect/platform-browser": "4.0.0-beta.32", - "@effect/platform-node": "4.0.0-beta.32", - "@effect/sql-pg": "4.0.0-beta.32", - "@effect/opentelemetry": "4.0.0-beta.32", - "@effect/atom-react": "4.0.0-beta.32", - "@effect/ai-openrouter": "4.0.0-beta.32", - "@effect/vitest": "4.0.0-beta.32" + "effect": "4.0.0-beta.33", + "@effect/platform-bun": "4.0.0-beta.33", + "@effect/platform-browser": "4.0.0-beta.33", + "@effect/platform-node": "4.0.0-beta.33", + "@effect/sql-pg": "4.0.0-beta.33", + "@effect/opentelemetry": "4.0.0-beta.33", + "@effect/atom-react": "4.0.0-beta.33", + "@effect/ai-openrouter": "4.0.0-beta.33", + "@effect/vitest": "4.0.0-beta.33" } } }, @@ -56,4 +56,4 @@ }, "packageManager": "bun@1.3.10", "trustedDependencies": [] -} +} \ No newline at end of file diff --git a/packages/domain/src/errors.ts b/packages/domain/src/errors.ts index 87932e552..c9f0c0782 100644 --- a/packages/domain/src/errors.ts +++ b/packages/domain/src/errors.ts @@ -1,4 +1,4 @@ -import { Effect, Predicate, Schema } from "effect" +import { Effect, Predicate, Schema, SchemaIssue } from "effect" import { ChannelId, MessageId } from "@hazel/schema" export class UnauthorizedError extends Schema.TaggedErrorClass("UnauthorizedError")( @@ -108,46 +108,39 @@ export class WorkflowServiceUnavailableError extends Schema.TaggedErrorClass( entityType: string, action: "update" | "create" | "delete" | "select", - entityId?: any | { value: any; key: string }[], + entityId?: unknown | { value: unknown; key: string }[], ) { return ( effect: Effect.Effect, - ): Effect.Effect | InternalServerError, A> => { + ): Effect.Effect | InternalServerError, A> => { + const toInternalError = (err: unknown, detailPrefix: string) => + Effect.fail( + new InternalServerError({ + message: `Error ${action}ing ${entityType}`, + detail: constructDetailMessage(detailPrefix, entityType, entityId), + cause: String(err), + }), + ) + return effect.pipe( - Effect.catchTags({ - DatabaseError: (err: any) => - Effect.fail( - new InternalServerError({ - message: `Error ${action}ing ${entityType}`, - detail: constructDetailMessage( - "There was an error in parsing when", - entityType, - entityId, - ), - cause: String(err), - }), - ), - ParseError: (err: any) => - Effect.fail( - new InternalServerError({ - message: `Error ${action}ing ${entityType}`, - detail: constructDetailMessage( - "There was an error in parsing when", - entityType, - entityId, - ), - cause: String(err), - }), - ), - }), - ) + Effect.catchIf( + (e): e is Extract => + Predicate.isTagged(e, "DatabaseError"), + (err) => toInternalError(err, "There was a database error when"), + ), + Effect.catchIf( + (e): e is Extract => + Predicate.isTagged(e, "SchemaError"), + (err) => toInternalError(err, "There was an error in parsing when"), + ), + ) as Effect.Effect | InternalServerError, A> } } const constructDetailMessage = ( title: string, entityType: string, - entityId?: any | { value: any; key: string }[], + entityId?: unknown | { value: unknown; key: string }[], ) => { if (entityId) { if (Array.isArray(entityId)) { From fb6ca7dfdb06521b2157d050cda5f00817b724fa Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 13:12:35 +0100 Subject: [PATCH 15/34] fix --- apps/backend/scripts/create-bot.ts | 2 +- .../backend/scripts/rebuild-channel-access.ts | 2 +- apps/backend/scripts/reset-all.ts | 2 +- apps/backend/scripts/seed-internal-bots.ts | 2 +- apps/backend/scripts/setup.ts | 2 +- apps/backend/src/routes/auth.http.ts | 42 +++++++--- apps/backend/src/routes/bot-commands.http.ts | 19 ++--- apps/backend/src/routes/chat-sync.http.ts | 10 ++- .../src/routes/incoming-webhooks.http.ts | 6 +- apps/backend/src/routes/integrations.http.ts | 81 ++++++++++++------- apps/backend/src/routes/klipy.http.ts | 3 +- apps/backend/src/routes/mock-data.http.ts | 4 +- apps/backend/src/routes/uploads.http.ts | 2 +- apps/backend/src/rpc/handlers/channels.ts | 24 +----- apps/backend/src/rpc/handlers/chat-sync.ts | 4 +- .../backend/src/rpc/handlers/organizations.ts | 33 +++++--- .../chat-sync/chat-sync-core-worker.ts | 24 +++--- .../src/services/message-outbox-dispatcher.ts | 10 +-- .../src/services/oauth/oauth-http-client.ts | 57 +++++-------- 19 files changed, 178 insertions(+), 151 deletions(-) diff --git a/apps/backend/scripts/create-bot.ts b/apps/backend/scripts/create-bot.ts index 69430835a..49100f362 100644 --- a/apps/backend/scripts/create-bot.ts +++ b/apps/backend/scripts/create-bot.ts @@ -138,7 +138,7 @@ const createBotScript = Effect.gen(function* () { // Run the script with proper Effect runtime const runnable = createBotScript.pipe( Effect.provide(DatabaseLive), - Effect.provide(Logger.minimumLogLevel(LogLevel.Info)), + Effect.provide(Layer.succeed(References.MinimumLogLevel, LogLevel.Info)), ) Effect.runPromise(runnable).catch((error) => { diff --git a/apps/backend/scripts/rebuild-channel-access.ts b/apps/backend/scripts/rebuild-channel-access.ts index d246547e5..ca730f7c4 100644 --- a/apps/backend/scripts/rebuild-channel-access.ts +++ b/apps/backend/scripts/rebuild-channel-access.ts @@ -52,7 +52,7 @@ Effect.runPromise( rebuildChannelAccess.pipe( Effect.provide(ChannelAccessSyncLive), Effect.provide(DatabaseLive), - Effect.provide(Logger.minimumLogLevel(LogLevel.Info)), + Effect.provide(Layer.succeed(References.MinimumLogLevel, LogLevel.Info)), ), ).catch((error) => { console.error("Failed to rebuild channel_access", error) diff --git a/apps/backend/scripts/reset-all.ts b/apps/backend/scripts/reset-all.ts index 93df946e1..253209f09 100644 --- a/apps/backend/scripts/reset-all.ts +++ b/apps/backend/scripts/reset-all.ts @@ -258,7 +258,7 @@ const resetScript = Effect.gen(function* () { const runnable = resetScript.pipe( Effect.provide(DatabaseLive), Effect.provide(WorkOSClient.layer), - Effect.provide(Logger.minimumLogLevel(LogLevel.Info)), + Effect.provide(Layer.succeed(References.MinimumLogLevel, LogLevel.Info)), ) Effect.runPromise(runnable).catch((error) => { diff --git a/apps/backend/scripts/seed-internal-bots.ts b/apps/backend/scripts/seed-internal-bots.ts index 85c34733f..d515105cd 100644 --- a/apps/backend/scripts/seed-internal-bots.ts +++ b/apps/backend/scripts/seed-internal-bots.ts @@ -217,7 +217,7 @@ const seedInternalBots = Effect.gen(function* () { // Run the script const runnable = seedInternalBots.pipe( Effect.provide(DatabaseLive), - Effect.provide(Logger.minimumLogLevel(LogLevel.Info)), + Effect.provide(Layer.succeed(References.MinimumLogLevel, LogLevel.Info)), ) Effect.runPromise(runnable).catch((error) => { diff --git a/apps/backend/scripts/setup.ts b/apps/backend/scripts/setup.ts index 3375e4eb0..92fa8acc6 100644 --- a/apps/backend/scripts/setup.ts +++ b/apps/backend/scripts/setup.ts @@ -123,7 +123,7 @@ const MainLive = Layer.mergeAll(WorkOSSync.layer, WorkOSClient.layer).pipe( const runnable = setupScript.pipe( Effect.provide(MainLive), - Effect.provide(Logger.minimumLogLevel(LogLevel.Info)), + Effect.provide(Layer.succeed(References.MinimumLogLevel, LogLevel.Info)), ) Effect.runPromise(runnable).catch((error) => { diff --git a/apps/backend/src/routes/auth.http.ts b/apps/backend/src/routes/auth.http.ts index 26496b927..c2fa018de 100644 --- a/apps/backend/src/routes/auth.http.ts +++ b/apps/backend/src/routes/auth.http.ts @@ -20,7 +20,7 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => // Validate returnTo is a relative URL (defense in depth) const validatedReturnTo = Schema.decodeSync(RelativeUrl)(query.returnTo) - const state = JSON.stringify(AuthState.make({ returnTo: validatedReturnTo })) + const state = JSON.stringify({ returnTo: validatedReturnTo }) let workosOrgId: string @@ -79,7 +79,11 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => Location: authorizationUrl, }, }) - }), + }).pipe( + Effect.catchTag("ConfigError", (err) => + Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + ), + ), ) .handle("callback", ({ query }) => Effect.gen(function* () { @@ -109,7 +113,11 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => Location: callbackUrl.toString(), }, }) - }), + }).pipe( + Effect.catchTag("ConfigError", (err) => + Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + ), + ), ) .handle("logout", ({ query }) => Effect.gen(function* () { @@ -124,7 +132,11 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => Location: returnTo, }, }) - }), + }).pipe( + Effect.catchTag("ConfigError", (err) => + Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + ), + ), ) .handle("loginDesktop", ({ query }) => Effect.gen(function* () { @@ -140,11 +152,11 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => const validatedReturnTo = Schema.decodeSync(RelativeUrl)(query.returnTo) // Build state with desktop connection info - const stateObj = DesktopAuthState.make({ + const stateObj = { returnTo: validatedReturnTo, desktopPort: query.desktopPort, desktopNonce: query.desktopNonce, - }) + } const state = JSON.stringify(stateObj) let workosOrgId: string | undefined @@ -188,7 +200,11 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => Location: authorizationUrl, }, }) - }), + }).pipe( + Effect.catchTag("ConfigError", (err) => + Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + ), + ), ) .handle("token", ({ payload }) => Effect.gen(function* () { @@ -292,7 +308,11 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => lastName: workosUser.lastName || "", }, } - }), + }).pipe( + Effect.catchTag("ConfigError", (err) => + Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + ), + ), ) .handle("refresh", ({ payload }) => Effect.gen(function* () { @@ -327,6 +347,10 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => refreshToken: authResponse.refreshToken!, expiresIn, } - }), + }).pipe( + Effect.catchTag("ConfigError", (err) => + Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + ), + ), ), ) diff --git a/apps/backend/src/routes/bot-commands.http.ts b/apps/backend/src/routes/bot-commands.http.ts index ed2e1e7ad..5d52d63ba 100644 --- a/apps/backend/src/routes/bot-commands.http.ts +++ b/apps/backend/src/routes/bot-commands.http.ts @@ -18,7 +18,7 @@ import { UpdateBotSettingsResponse, } from "@hazel/domain/http" import { Redis } from "@hazel/effect-bun" -import { ServiceMap, Duration, Effect, Option, Schedule, Stream } from "effect" +import { ServiceMap, Cause, Duration, Effect, Option, Queue, Schedule, Stream } from "effect" import { HazelApi } from "../api.ts" import { BotGatewayService } from "../services/bot-gateway-service.ts" import { IntegrationTokenService } from "../services/integration-token-service.ts" @@ -78,7 +78,7 @@ const encodeSseEvent = (event: string, data: string) => data, }) -export const createSseHeartbeatStream = (interval: Duration.DurationInput = HEARTBEAT_INTERVAL) => +export const createSseHeartbeatStream = (interval: Duration.Input = HEARTBEAT_INTERVAL) => Stream.make( encodeSseEvent( "heartbeat", @@ -108,7 +108,7 @@ interface CommandSseStreamOptions { readonly botName: string readonly channel: string readonly redis: Pick, "subscribe"> - readonly heartbeatInterval?: Duration.DurationInput + readonly heartbeatInterval?: Duration.Input } export const createCommandSseStream = ({ @@ -118,11 +118,11 @@ export const createCommandSseStream = ({ redis, heartbeatInterval = HEARTBEAT_INTERVAL, }: CommandSseStreamOptions) => { - const commandStream = Stream.async((emit) => { + const commandStream = Stream.callback((queue) => Effect.gen(function* () { const { unsubscribe } = yield* redis.subscribe(channel, (message) => { // Encode the message as an SSE event - emit.single(encodeSseEvent("command", message)) + Queue.offerUnsafe(queue, encodeSseEvent("command", message)) }) // Add finalizer to unsubscribe when stream closes @@ -138,16 +138,13 @@ export const createCommandSseStream = ({ // Keep the subscription alive until the stream is closed yield* Effect.never }).pipe( - Effect.scoped, Effect.catch((error) => { // Log the error but don't fail the stream - end it gracefully Effect.runFork(Effect.logError("Redis subscription error", { error, botId, botName })) - emit.end() - return Effect.void + return Queue.end(queue) }), - Effect.runFork, - ) - }) + ), + ) return Stream.merge(commandStream, createSseHeartbeatStream(heartbeatInterval), { haltStrategy: "either", diff --git a/apps/backend/src/routes/chat-sync.http.ts b/apps/backend/src/routes/chat-sync.http.ts index f021f4762..e1cad8d6f 100644 --- a/apps/backend/src/routes/chat-sync.http.ts +++ b/apps/backend/src/routes/chat-sync.http.ts @@ -121,7 +121,7 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han const txid = yield* generateTransactionId() return new ChatSyncConnectionResponse({ data: connection, transactionId: txid }) }).pipe( - Effect.catchTag("ParseError", (error) => + Effect.catchTag("SchemaError", (error) => Effect.fail(toInternalServerError("Invalid sync connection data", error)), ), Effect.catchTag("DatabaseError", (error) => @@ -172,6 +172,9 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han toInternalServerError("Database error while deleting sync connection", error), ), ), + Effect.catchTag("SchemaError", (error) => + Effect.fail(toInternalServerError("Schema error while deleting sync connection", error)), + ), ), ) .handle("createChannelLink", ({ params, payload }) => @@ -234,7 +237,7 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han const txid = yield* generateTransactionId() return new ChatSyncChannelLinkResponse({ data: brandedLink, transactionId: txid }) }).pipe( - Effect.catchTag("ParseError", (error) => + Effect.catchTag("SchemaError", (error) => Effect.fail(toInternalServerError("Invalid channel link data", error)), ), Effect.catchTag("DatabaseError", (error) => @@ -298,6 +301,9 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han toInternalServerError("Database error while deleting channel link", error), ), ), + Effect.catchTag("SchemaError", (error) => + Effect.fail(toInternalServerError("Schema error while deleting channel link", error)), + ), ), ) }), diff --git a/apps/backend/src/routes/incoming-webhooks.http.ts b/apps/backend/src/routes/incoming-webhooks.http.ts index 5da015f23..a5c103dee 100644 --- a/apps/backend/src/routes/incoming-webhooks.http.ts +++ b/apps/backend/src/routes/incoming-webhooks.http.ts @@ -154,7 +154,7 @@ export const HttpIncomingWebhookLive = HttpApiBuilder.group(HazelApi, "incoming- detail: String(error), }), ), - ParseError: (error: unknown) => + SchemaError: (error: unknown) => Effect.fail( new InternalServerError({ message: "Invalid request data", @@ -257,7 +257,7 @@ export const HttpIncomingWebhookLive = HttpApiBuilder.group(HazelApi, "incoming- detail: String(error), }), ), - ParseError: (error: unknown) => + SchemaError: (error: unknown) => Effect.fail( new InternalServerError({ message: "Invalid request data", @@ -357,7 +357,7 @@ export const HttpIncomingWebhookLive = HttpApiBuilder.group(HazelApi, "incoming- detail: String(error), }), ), - ParseError: (error: unknown) => + SchemaError: (error: unknown) => Effect.fail( new InternalServerError({ message: "Invalid request data", diff --git a/apps/backend/src/routes/integrations.http.ts b/apps/backend/src/routes/integrations.http.ts index c0b5da702..6da0bd544 100644 --- a/apps/backend/src/routes/integrations.http.ts +++ b/apps/backend/src/routes/integrations.http.ts @@ -18,7 +18,7 @@ import { CraftRateLimitError, } from "@hazel/integrations/craft" import type { IntegrationConnection } from "@hazel/domain/models" -import { Config, Effect, Layer, Option, Schedule, Schema } from "effect" +import { Config, DateTime, Effect, Layer, Option, Schedule, Schema, SchemaGetter } from "effect" import * as Duration from "effect/Duration" import { HazelApi } from "../api" import { ChatSyncAttributionReconciler } from "../services/chat-sync/chat-sync-attribution-reconciler" @@ -33,13 +33,16 @@ import { OAuthProviderRegistry } from "../services/oauth" const OAuthState = Schema.Struct({ organizationId: Schema.String, userId: Schema.String, - level: Schema.optionalWith(Schema.Literals(["organization", "user"]), { - default: () => "organization", - }), + level: Schema.optional(Schema.Literals(["organization", "user"])).pipe( + Schema.decodeTo(Schema.toType(Schema.Literals(["organization", "user"])), { + decode: SchemaGetter.withDefault((): "organization" | "user" => "organization"), + encode: SchemaGetter.required(), + }), + ), /** Full URL to redirect after OAuth completes (e.g., http://localhost:3000/org/settings/integrations/github) */ returnTo: Schema.String, /** Environment that initiated the OAuth flow. Used to redirect back to localhost for local dev. */ - environment: Schema.optional(Schema.Literals(["local", "production"])), + environment: Schema.optionalKey(Schema.Literals(["local", "production"])), }) /** @@ -248,8 +251,8 @@ const makeOAuthSessionCookie = ( ) => Effect.try({ try: () => - Cookies.unsafeMakeCookie(name, value, { - domain: options.cookieDomain, params: "/", + Cookies.makeCookieUnsafe(name, value, { + domain: options.cookieDomain, path: "/", httpOnly: true, secure: options.secure, sameSite: "lax", @@ -264,7 +267,7 @@ const makeOAuthSessionCookie = ( const expireOAuthSessionCookie = (name: string, options: { cookieDomain: string; secure: boolean }) => HttpServerResponse.expireCookie(name, { - domain: options.cookieDomain, params: "/", + domain: options.cookieDomain, path: "/", httpOnly: true, secure: options.secure, sameSite: "lax", @@ -412,11 +415,14 @@ const handleGetOAuthUrl = Effect.fn("integrations.getOAuthUrl")(function* ( const OAuthSessionState = Schema.Struct({ organizationId: Schema.String, userId: Schema.String, - level: Schema.optionalWith(Schema.Literals(["organization", "user"]), { - default: () => "organization", - }), + level: Schema.optional(Schema.Literals(["organization", "user"])).pipe( + Schema.decodeTo(Schema.toType(Schema.Literals(["organization", "user"])), { + decode: SchemaGetter.withDefault((): "organization" | "user" => "organization"), + encode: SchemaGetter.required(), + }), + ), returnTo: Schema.String, - environment: Schema.optional(Schema.Literals(["local", "production"])), + environment: Schema.optionalKey(Schema.Literals(["local", "production"])), createdAt: Schema.Number, }) @@ -709,7 +715,7 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( ) if (tokensResult._tag === "Failure") { - const error = tokensResult.left + const error = tokensResult.failure yield* Effect.logError("OAuth token exchange failed", { event: "integration_token_exchange_failed", provider, @@ -719,7 +725,7 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( return redirectWithError("token_exchange_failed") } - const tokens = tokensResult.value + const tokens = tokensResult.success yield* Effect.logInfo("OAuth token exchange succeeded", { event: "integration_token_exchange_success", provider, @@ -742,7 +748,7 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( ) if (accountInfoResult._tag === "Failure") { - const error = accountInfoResult.left + const error = accountInfoResult.failure yield* Effect.logError("OAuth account info fetch failed", { event: "integration_account_info_failed", provider, @@ -751,7 +757,7 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( return redirectWithError("account_info_failed") } - const accountInfo = accountInfoResult.value + const accountInfo = accountInfoResult.success yield* Effect.logDebug("OAuth account info fetch succeeded", { event: "integration_account_info_success", provider, @@ -816,12 +822,12 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( yield* Effect.logError("OAuth database upsert failed", { event: "integration_db_upsert_failed", provider, - error: String(connectionResult.left), + error: String(connectionResult.failure), }) return redirectWithError("db_error") } - const connection = connectionResult.value + const connection = connectionResult.success yield* Effect.logDebug("OAuth database upsert succeeded", { event: "integration_db_upsert_success", provider, @@ -849,7 +855,7 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( event: "integration_token_storage_failed", provider, connectionId: connection.id, - error: String(storeResult.left), + error: String(storeResult.failure), }) return redirectWithError("encryption_error") } @@ -885,7 +891,7 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( organizationId: parsedState.organizationId, userId: parsedState.userId, externalAccountId: accountInfo.externalAccountId, - error: String(reconcileResult.left), + error: String(reconcileResult.failure), }, ) } @@ -1102,8 +1108,8 @@ const handleGetConnectionStatus = Effect.fn("integrations.getConnectionStatus")( provider, externalAccountName: connection.externalAccountName, status: connection.status, - connectedAt: connection.createdAt ?? null, - lastUsedAt: connection.lastUsedAt ?? null, + connectedAt: connection.createdAt ? DateTime.fromDateUnsafe(connection.createdAt) : null, + lastUsedAt: connection.lastUsedAt ? DateTime.fromDateUnsafe(connection.lastUsedAt) : null, }) }) @@ -1169,7 +1175,7 @@ const handleDisconnect = Effect.fn("integrations.disconnect")(function* ( organizationId: orgId, userId: currentUser.id, externalAccountId, - error: String(reconcileResult.left), + error: String(reconcileResult.failure), }, ) } @@ -1195,9 +1201,15 @@ const handleDisconnect = Effect.fn("integrations.disconnect")(function* ( export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations", (handlers) => handlers - .handle("getOAuthUrl", ({ params, query }) => handleGetOAuthUrl(path, query)) + .handle("getOAuthUrl", ({ params, query }) => + handleGetOAuthUrl(params, query).pipe( + Effect.catchTag("ConfigError", (err) => + Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + ), + ), + ) .handle("oauthCallback", ({ params, query }) => - handleOAuthCallback(path, query).pipe( + handleOAuthCallback(params, query).pipe( Effect.catchTag("DatabaseError", (error) => Effect.fail( new InternalServerError({ @@ -1206,10 +1218,21 @@ export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations" }), ), ), + Effect.catchTag("SchemaError", (error) => + Effect.fail( + new InternalServerError({ + message: "Schema error during OAuth callback", + detail: String(error), + }), + ), + ), + Effect.catchTag("ConfigError", (err) => + Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + ), ), ) .handle("connectApiKey", ({ params, payload }) => - handleConnectApiKey(path, payload).pipe( + handleConnectApiKey(params, payload).pipe( Effect.catchTags({ DatabaseError: (error) => Effect.fail( @@ -1218,7 +1241,7 @@ export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations" detail: String(error), }), ), - ParseError: (error) => + SchemaError: (error) => Effect.fail( new InternalServerError({ message: "Failed to parse API response", @@ -1236,7 +1259,7 @@ export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations" ), ) .handle("getConnectionStatus", ({ params, query }) => - handleGetConnectionStatus(path, query).pipe( + handleGetConnectionStatus(params, query).pipe( Effect.catchTag("DatabaseError", (error) => Effect.fail( new InternalServerError({ @@ -1248,7 +1271,7 @@ export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations" ), ) .handle("disconnect", ({ params, query }) => - handleDisconnect(path, query).pipe( + handleDisconnect(params, query).pipe( Effect.catchTag("DatabaseError", (error) => Effect.fail( new InternalServerError({ diff --git a/apps/backend/src/routes/klipy.http.ts b/apps/backend/src/routes/klipy.http.ts index 8341a3fd2..9f394464c 100644 --- a/apps/backend/src/routes/klipy.http.ts +++ b/apps/backend/src/routes/klipy.http.ts @@ -62,7 +62,8 @@ const KlipyRawCategoriesResponse = Schema.Struct({ const fetchKlipy = ( httpClient: HttpClient.HttpClient, - apiKey: string, params: string, + apiKey: string, + path: string, params: Record, ) => { const searchParams = new URLSearchParams(params) diff --git a/apps/backend/src/routes/mock-data.http.ts b/apps/backend/src/routes/mock-data.http.ts index 7e5cebcdc..d96f87ed2 100644 --- a/apps/backend/src/routes/mock-data.http.ts +++ b/apps/backend/src/routes/mock-data.http.ts @@ -21,8 +21,8 @@ export const HttpMockDataLive = HttpApiBuilder.group(HazelApi, "mockData", (hand .transaction( Effect.gen(function* () { const result = yield* mockDataService.generateForMarketingScreenshots({ - organizationId: OrganizationId.make(payload.organizationId), - currentUserId: UserId.make(currentUser.id), + organizationId: OrganizationId.makeUnsafe(payload.organizationId), + currentUserId: UserId.makeUnsafe(currentUser.id), }) const txid = yield* generateTransactionId() diff --git a/apps/backend/src/routes/uploads.http.ts b/apps/backend/src/routes/uploads.http.ts index 240ff009a..c241baee6 100644 --- a/apps/backend/src/routes/uploads.http.ts +++ b/apps/backend/src/routes/uploads.http.ts @@ -232,7 +232,7 @@ export const HttpUploadsLive = HttpApiBuilder.group(HazelApi, "uploads", (handle // ============ Attachment Upload ============ Match.when({ type: "attachment" }, (req) => Effect.gen(function* () { - const attachmentId = AttachmentId.make(randomUUIDv7()) + const attachmentId = AttachmentId.makeUnsafe(randomUUIDv7()) yield* Effect.logDebug( `Generating presigned URL for attachment upload: ${attachmentId} (size: ${req.fileSize} bytes, type: ${req.contentType})`, diff --git a/apps/backend/src/rpc/handlers/channels.ts b/apps/backend/src/rpc/handlers/channels.ts index ca93e1e36..7575c7997 100644 --- a/apps/backend/src/rpc/handlers/channels.ts +++ b/apps/backend/src/rpc/handlers/channels.ts @@ -204,7 +204,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( const existingChannel = yield* channelMemberRepo.findExistingSingleDmChannel( user.id, payload.participantIds[0], - OrganizationId.make(payload.organizationId), + OrganizationId.makeUnsafe(payload.organizationId), ) if (Option.isSome(existingChannel)) { @@ -236,12 +236,12 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( } // Create channel - yield* channelPolicy.canCreate(OrganizationId.make(payload.organizationId)) + yield* channelPolicy.canCreate(OrganizationId.makeUnsafe(payload.organizationId)) const createdChannel = yield* channelRepo.insert({ name: channelName || "Group Channel", icon: null, type: payload.type, - organizationId: OrganizationId.make(payload.organizationId), + organizationId: OrganizationId.makeUnsafe(payload.organizationId), parentChannelId: null, sectionId: null, deletedAt: null, @@ -506,23 +506,7 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( }), ), ), - Effect.catchTag("HttpClientError", (err) => - Effect.fail( - new InternalServerError({ - message: `Thread naming failed: ${err.reason}`, - cause: String(err), - }), - ), - ), - Effect.catchTag("HttpApiDecodeError", (err) => - Effect.fail( - new InternalServerError({ - message: "Failed to decode workflow response", - cause: String(err), - }), - ), - ), - Effect.catchTag("ParseError", (err) => + Effect.catchTag("SchemaError", (err) => Effect.fail( new InternalServerError({ message: "Failed to parse workflow response", diff --git a/apps/backend/src/rpc/handlers/chat-sync.ts b/apps/backend/src/rpc/handlers/chat-sync.ts index 404e89962..7cc0359b2 100644 --- a/apps/backend/src/rpc/handlers/chat-sync.ts +++ b/apps/backend/src/rpc/handlers/chat-sync.ts @@ -115,7 +115,7 @@ export const ChatSyncRpcLive = ChatSyncRpcs.toLayer( }), ) .pipe( - Effect.catchTag("ParseError", (error) => + Effect.catchTag("SchemaError", (error) => Effect.fail( new InternalServerError({ message: "Invalid sync connection data", @@ -252,7 +252,7 @@ export const ChatSyncRpcLive = ChatSyncRpcs.toLayer( }), ) .pipe( - Effect.catchTag("ParseError", (error) => + Effect.catchTag("SchemaError", (error) => Effect.fail( new InternalServerError({ message: "Invalid channel link data", diff --git a/apps/backend/src/rpc/handlers/organizations.ts b/apps/backend/src/rpc/handlers/organizations.ts index 40d95fb44..90ba4da92 100644 --- a/apps/backend/src/rpc/handlers/organizations.ts +++ b/apps/backend/src/rpc/handlers/organizations.ts @@ -15,7 +15,7 @@ import { OrganizationSlugAlreadyExistsError, PublicInviteDisabledError, } from "@hazel/domain/rpc" -import { Effect, Option } from "effect" +import { Effect, Option, Predicate } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" import { OrganizationPolicy } from "../../policies/organization-policy" import { ChannelAccessSyncService } from "../../services/channel-access-sync" @@ -33,21 +33,24 @@ const handleOrganizationDbErrors = ( effect: Effect.Effect, ): Effect.Effect< R, - | Exclude + | Exclude | InternalServerError | OrganizationSlugAlreadyExistsError, A > => { return effect.pipe( - Effect.catchTags({ - DatabaseError: (err: any) => { + Effect.catchIf( + (e): e is Extract => + Predicate.isTagged(e, "DatabaseError"), + (err) => { + const dbErr = err as unknown as { type: string; cause: { constraint_name?: string; detail?: string } } // Check if it's a unique violation on the slug column if ( - err.type === "unique_violation" && - err.cause.constraint_name === "organizations_slug_unique" + dbErr.type === "unique_violation" && + dbErr.cause.constraint_name === "organizations_slug_unique" ) { // Extract slug from error detail if possible - const slugMatch = err.cause.detail?.match(/Key \(slug\)=\(([^)]+)\)/) + const slugMatch = dbErr.cause.detail?.match(/Key \(slug\)=\(([^)]+)\)/) const slug = slugMatch?.[1] || "unknown" return Effect.fail( new OrganizationSlugAlreadyExistsError({ @@ -65,7 +68,11 @@ const handleOrganizationDbErrors = ( }), ) }, - ParseError: (err: any) => + ), + Effect.catchIf( + (e): e is Extract => + Predicate.isTagged(e, "SchemaError"), + (err) => Effect.fail( new InternalServerError({ message: `Error ${action}ing ${entityType}`, @@ -73,8 +80,14 @@ const handleOrganizationDbErrors = ( cause: String(err), }), ), - }), - ) + ), + ) as Effect.Effect< + R, + | Exclude + | InternalServerError + | OrganizationSlugAlreadyExistsError, + A + > } } diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index 7c039d991..beb7bee28 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -1279,7 +1279,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() unsyncedMessage.id, ).pipe(Effect.result) if (result._tag === "Success") { - if (result.value.status === "synced") { + if (result.success.status === "synced") { sent++ } else { skipped++ @@ -1290,7 +1290,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() provider: connection.provider, syncConnectionId, hazelMessageId: unsyncedMessage.id, - error: result.left, + error: result.failure, }) } } @@ -1751,7 +1751,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() dedupeKey, ).pipe(Effect.result) if (result._tag === "Success") { - if (result.value.status === "synced" || result.value.status === "already_linked") { + if (result.success.status === "synced" || result.success.status === "already_linked") { synced++ } } else { @@ -1760,7 +1760,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() provider, hazelMessageId, syncConnectionId: target.syncConnectionId, - error: result.left, + error: result.failure, }) } } @@ -1787,7 +1787,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() dedupeKey, ).pipe(Effect.result) if (result._tag === "Success") { - if (result.value.status === "updated") { + if (result.success.status === "updated") { synced++ } } else { @@ -1796,7 +1796,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() provider, hazelMessageId, syncConnectionId: target.syncConnectionId, - error: result.left, + error: result.failure, }) } } @@ -1823,7 +1823,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() dedupeKey, ).pipe(Effect.result) if (result._tag === "Success") { - if (result.value.status === "deleted") { + if (result.success.status === "deleted") { synced++ } } else { @@ -1832,7 +1832,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() provider, hazelMessageId, syncConnectionId: target.syncConnectionId, - error: result.left, + error: result.failure, }) } } @@ -1860,7 +1860,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() dedupeKey, ).pipe(Effect.result) if (result._tag === "Success") { - if (result.value.status === "created") { + if (result.success.status === "created") { synced++ } } else { @@ -1869,7 +1869,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() provider, hazelReactionId, syncConnectionId: target.syncConnectionId, - error: result.left, + error: result.failure, }) } } @@ -1901,7 +1901,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() dedupeKey, ).pipe(Effect.result) if (result._tag === "Success") { - if (result.value.status === "deleted") { + if (result.success.status === "deleted") { synced++ } } else { @@ -1910,7 +1910,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() provider, hazelMessageId: payload.hazelMessageId, syncConnectionId: target.syncConnectionId, - error: result.left, + error: result.failure, }) } } diff --git a/apps/backend/src/services/message-outbox-dispatcher.ts b/apps/backend/src/services/message-outbox-dispatcher.ts index cfbc8bd65..9ef3efbd3 100644 --- a/apps/backend/src/services/message-outbox-dispatcher.ts +++ b/apps/backend/src/services/message-outbox-dispatcher.ts @@ -118,7 +118,7 @@ export class MessageOutboxDispatcher extends ServiceMap.Service= OUTBOX_FAILURE_LIMIT) { yield* outboxRepo.markFailed(event.id, { @@ -178,12 +178,12 @@ export class MessageOutboxDispatcher extends ServiceMap.Service @@ -195,14 +195,14 @@ export class MessageOutboxDispatcher extends ServiceMap.Service reserved.release()) yield* Effect.sleep(OUTBOX_LOCK_RETRY_INTERVAL) return yield* campaignForLeadership() } - const lockRows = lockResult.value as { rows: Array<{ locked: boolean }> } + const lockRows = lockResult.success as { rows: Array<{ locked: boolean }> } if (!lockRows.rows[0]?.locked) { yield* Effect.sync(() => reserved.release()) yield* Effect.sleep(OUTBOX_LOCK_RETRY_INTERVAL) diff --git a/apps/backend/src/services/oauth/oauth-http-client.ts b/apps/backend/src/services/oauth/oauth-http-client.ts index eae445596..1c267331c 100644 --- a/apps/backend/src/services/oauth/oauth-http-client.ts +++ b/apps/backend/src/services/oauth/oauth-http-client.ts @@ -6,7 +6,7 @@ */ import { FetchHttpClient, HttpBody, HttpClient } from "effect/unstable/http" -import { ServiceMap, Duration, Effect, Layer, Schema } from "effect" +import { ServiceMap, Duration, Effect, Layer, Predicate, Schema, SchemaGetter, SchemaIssue } from "effect" import type { OAuthIntegrationProvider } from "./provider-config" // ============================================================================ @@ -21,10 +21,15 @@ const DEFAULT_TIMEOUT = Duration.seconds(30) const OAuthTokenApiResponse = Schema.Struct({ access_token: Schema.String, - refresh_token: Schema.optional(Schema.String), - expires_in: Schema.optional(Schema.Number), - scope: Schema.optional(Schema.String), - token_type: Schema.optional(Schema.String, { default: () => "Bearer" }), + refresh_token: Schema.optionalKey(Schema.String), + expires_in: Schema.optionalKey(Schema.Number), + scope: Schema.optionalKey(Schema.String), + token_type: Schema.optional(Schema.String).pipe( + Schema.decodeTo(Schema.toType(Schema.String), { + decode: SchemaGetter.withDefault(() => "Bearer"), + encode: SchemaGetter.required(), + }), + ), }) // ============================================================================ @@ -126,18 +131,14 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut const data = yield* response.json.pipe( Effect.flatMap(Schema.decodeUnknownEffect(OAuthTokenApiResponse)), - Effect.catchTags({ - ParseError: (error) => + Effect.catch((error) => + Effect.fail( new OAuthHttpError({ message: `Failed to parse token response: ${String(error)}`, cause: error, }), - ResponseError: (error) => - new OAuthHttpError({ - message: `Failed to read response body: ${error.message}`, - cause: error, - }), - }), + ), + ), ) return { @@ -191,18 +192,14 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut const data = yield* response.json.pipe( Effect.flatMap(Schema.decodeUnknownEffect(OAuthTokenApiResponse)), - Effect.catchTags({ - ParseError: (error) => + Effect.catch((error) => + Effect.fail( new OAuthHttpError({ message: `Failed to parse token response: ${String(error)}`, cause: error, }), - ResponseError: (error) => - new OAuthHttpError({ - message: `Failed to read response body: ${error.message}`, - cause: error, - }), - }), + ), + ), ) return { @@ -231,15 +228,6 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut }), ), ), - Effect.catchTag("HttpClientError", (error) => - Effect.fail( - new OAuthHttpError({ - message: `Response error: ${String(error)}`, - status: error.response.status, - cause: error, - }), - ), - ), Effect.withSpan("OAuthHttpClient.exchangeCode"), ) @@ -259,15 +247,6 @@ export class OAuthHttpClient extends ServiceMap.Service()("OAut }), ), ), - Effect.catchTag("HttpClientError", (error) => - Effect.fail( - new OAuthHttpError({ - message: `Response error: ${String(error)}`, - status: error.response.status, - cause: error, - }), - ), - ), Effect.withSpan("OAuthHttpClient.refreshToken", { attributes: { provider } }), ) From 2391113fc0d9c73f3ca62743b5149ee2c3b55a3f Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 13:30:18 +0100 Subject: [PATCH 16/34] fix --- apps/backend/src/routes/webhooks.http.ts | 17 ++++--- .../src/services/bot-gateway-service.ts | 12 +++-- .../chat-sync/chat-sync-core-worker.ts | 4 +- .../chat-sync/chat-sync-provider-registry.ts | 2 +- .../chat-sync/discord-gateway-service.ts | 6 +-- apps/backend/src/services/database.ts | 15 +++---- .../src/services/integration-encryption.ts | 6 +-- .../services/message-side-effect-service.ts | 45 ++++--------------- .../src/services/oauth/provider-config.ts | 4 +- .../src/services/typing-indicator-cleanup.ts | 2 +- apps/backend/src/services/workos-auth.ts | 2 +- apps/backend/src/services/workos-webhook.ts | 10 ++--- 12 files changed, 53 insertions(+), 72 deletions(-) diff --git a/apps/backend/src/routes/webhooks.http.ts b/apps/backend/src/routes/webhooks.http.ts index 9e922adcf..144ad5ac9 100644 --- a/apps/backend/src/routes/webhooks.http.ts +++ b/apps/backend/src/routes/webhooks.http.ts @@ -1,6 +1,7 @@ import { createHmac, timingSafeEqual } from "node:crypto" import { HttpApiBuilder } from "effect/unstable/httpapi" import { HttpServerRequest } from "effect/unstable/http" +import { InternalServerError } from "@hazel/domain" import { GitHubWebhookResponse, InvalidGitHubWebhookSignature } from "@hazel/domain/http" import type { Event } from "@workos-inc/node" import { Config, Effect, pipe, Redacted } from "effect" @@ -25,7 +26,7 @@ export const HttpWebhookLive = HttpApiBuilder.group(HazelApi, "webhooks", (handl const rawBody = yield* pipe( request.text, - Effect.orElseFail( + Effect.mapError( () => new InvalidWebhookSignature({ message: "Invalid request body", @@ -101,7 +102,7 @@ export const HttpWebhookLive = HttpApiBuilder.group(HazelApi, "webhooks", (handl const rawBody = yield* pipe( request.text, - Effect.orElseFail( + Effect.mapError( () => new InvalidGitHubWebhookSignature({ message: "Invalid request body", @@ -110,10 +111,12 @@ export const HttpWebhookLive = HttpApiBuilder.group(HazelApi, "webhooks", (handl ) const skipSignatureVerification = yield* Config.boolean("GITHUB_WEBHOOK_SKIP_SIGNATURE").pipe( - Effect.orElseSucceed(() => false), + Config.withDefault(false), ) - const webhookSecret = yield* Config.redacted("GITHUB_WEBHOOK_SECRET").pipe( + const webhookSecret = yield* Effect.gen(function* () { + return yield* Config.redacted("GITHUB_WEBHOOK_SECRET") + }).pipe( Effect.catchTag("ConfigError", () => skipSignatureVerification ? Effect.succeed(Redacted.make("")) @@ -169,6 +172,10 @@ export const HttpWebhookLive = HttpApiBuilder.group(HazelApi, "webhooks", (handl processed: true, messagesCreated: 0, }) - }), + }).pipe( + Effect.catchTag("ConfigError", (err) => + Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + ), + ), ), ) diff --git a/apps/backend/src/services/bot-gateway-service.ts b/apps/backend/src/services/bot-gateway-service.ts index 157c03ffb..b886cb1a4 100644 --- a/apps/backend/src/services/bot-gateway-service.ts +++ b/apps/backend/src/services/bot-gateway-service.ts @@ -6,10 +6,14 @@ import { } from "@hazel/domain" import type { Channel, ChannelMember, Message } from "@hazel/domain/models" import type { BotId, ChannelId, OrganizationId } from "@hazel/schema" -import { ServiceMap, Config, Effect, Layer, Option, Ref, Schema } from "effect" +import { ServiceMap, Config, DateTime, Effect, Layer, Option, Ref, Schema } from "effect" const DEFAULT_DURABLE_STREAMS_URL = "http://localhost:4437/v1/stream" +/** Get epoch milliseconds from Date or DateTime.Utc */ +const toEpochMs = (d: Date | DateTime.Utc): number => + d instanceof Date ? d.getTime() : DateTime.toEpochMillis(d) + const normalizeBaseUrl = (value: string): string => value.replace(/\/+$/, "") const createDeliveryId = (): string => crypto.randomUUID() @@ -187,7 +191,7 @@ export class BotGatewayService extends ServiceMap.Service()(" } const eventTimestamp = - message.updatedAt?.getTime?.() ?? message.createdAt?.getTime?.() ?? Date.now() + message.updatedAt ? toEpochMs(message.updatedAt) : message.createdAt ? toEpochMs(message.createdAt) : Date.now() yield* publishToInstalledBots(organizationId, () => ({ schemaVersion: 1, @@ -208,7 +212,7 @@ export class BotGatewayService extends ServiceMap.Service()(" channel: Schema.Schema.Type, ) { const eventTimestamp = - channel.updatedAt?.getTime?.() ?? channel.createdAt?.getTime?.() ?? Date.now() + channel.updatedAt ? toEpochMs(channel.updatedAt) : channel.createdAt ? toEpochMs(channel.createdAt) : Date.now() yield* publishToInstalledBots(channel.organizationId, () => ({ schemaVersion: 1, @@ -233,7 +237,7 @@ export class BotGatewayService extends ServiceMap.Service()(" return } - const eventTimestamp = member.createdAt?.getTime?.() ?? member.joinedAt?.getTime?.() ?? Date.now() + const eventTimestamp = member.createdAt ? toEpochMs(member.createdAt) : member.joinedAt ? toEpochMs(member.joinedAt) : Date.now() yield* publishToInstalledBots(organizationId, () => ({ schemaVersion: 1, diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index beb7bee28..611260278 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -221,7 +221,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() const getAttachmentPublicUrlBase = Effect.fn("discordSyncWorker.getAttachmentPublicUrlBase")( function* () { - const configuredBaseUrl = yield* Effect.option(Config.string("S3_PUBLIC_URL")) + const configuredBaseUrl = yield* Config.string("S3_PUBLIC_URL").pipe(Config.option) if (Option.isNone(configuredBaseUrl) || configuredBaseUrl.value.trim().length === 0) { return yield* Effect.fail( new DiscordSyncConfigurationError({ @@ -500,7 +500,7 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() return currentConfig } - const botTokenOption = yield* Effect.option(Config.redacted("DISCORD_BOT_TOKEN")) + const botTokenOption = yield* Config.redacted("DISCORD_BOT_TOKEN").pipe(Config.option) if (Option.isNone(botTokenOption)) { return Option.none() } diff --git a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts index c29837f8a..16de84c69 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts @@ -92,7 +92,7 @@ export class ChatSyncProviderRegistry extends ServiceMap.Service, | "ingestMessageCreate" | "ingestMessageUpdate" | "ingestMessageDelete" @@ -580,12 +580,10 @@ export class DiscordGatewayService extends ServiceMap.Service - Database.layer({ - url: envVars.DATABASE_URL, - ssl: !envVars.IS_DEV, - }), - ), - ), + Effect.gen(function* () { + const envVars = yield* EnvVars + return Database.layer({ + url: envVars.DATABASE_URL, + ssl: !envVars.IS_DEV, + }) + }), ).pipe(Layer.provide(EnvVars.layer)) diff --git a/apps/backend/src/services/integration-encryption.ts b/apps/backend/src/services/integration-encryption.ts index d80cf63fc..453ae8422 100644 --- a/apps/backend/src/services/integration-encryption.ts +++ b/apps/backend/src/services/integration-encryption.ts @@ -1,4 +1,4 @@ -import { ServiceMap, Config, Effect, Option, Redacted, Schema } from "effect" +import { ServiceMap, Config, Effect, Layer, Option, Redacted, Schema } from "effect" export interface EncryptedToken { ciphertext: string // Base64 encoded @@ -34,10 +34,10 @@ export class IntegrationEncryption extends ServiceMap.Service - Effect.fail( - new WorkflowInitializationError({ - message: "Failed to execute notification workflow", - cause: err.message, - }), - ), - ParseError: (err) => - Effect.fail( - new WorkflowInitializationError({ - message: "Failed to execute notification workflow", - cause: String(err), - }), - ), - RequestError: (err) => - Effect.fail( - new WorkflowInitializationError({ - message: "Failed to execute notification workflow", - cause: err.message, - }), - ), - ResponseError: (err) => - Effect.fail( - new WorkflowInitializationError({ - message: "Failed to execute notification workflow", - cause: err.message, - }), - ), - }), + Effect.catch((err) => + Effect.fail( + new WorkflowInitializationError({ + message: "Failed to execute notification workflow", + cause: String(err), + }), + ), + ), ) if (channelType !== "thread") { @@ -198,12 +176,7 @@ export class MessageSideEffectService extends ServiceMap.Service Effect.void, - ParseError: () => Effect.void, - RequestError: () => Effect.void, - ResponseError: () => Effect.void, - }), + Effect.catch(() => Effect.void), ) }, ) diff --git a/apps/backend/src/services/oauth/provider-config.ts b/apps/backend/src/services/oauth/provider-config.ts index 5059d028a..fc6b0b9db 100644 --- a/apps/backend/src/services/oauth/provider-config.ts +++ b/apps/backend/src/services/oauth/provider-config.ts @@ -124,12 +124,12 @@ export const loadProviderConfig = (provider: OAuthIntegrationProvider) => { const prefix = provider.toUpperCase() const staticConfig = PROVIDER_CONFIGS[provider] - return Effect.all({ + return Config.all({ apiBaseUrl: Config.string("API_BASE_URL"), clientId: Config.string(`${prefix}_CLIENT_ID`), clientSecret: Config.redacted(`${prefix}_CLIENT_SECRET`), }).pipe( - Effect.map( + Config.map( (envConfig): OAuthProviderConfig => ({ ...staticConfig, clientId: envConfig.clientId, diff --git a/apps/backend/src/services/typing-indicator-cleanup.ts b/apps/backend/src/services/typing-indicator-cleanup.ts index 233a14063..d7694f6b6 100644 --- a/apps/backend/src/services/typing-indicator-cleanup.ts +++ b/apps/backend/src/services/typing-indicator-cleanup.ts @@ -25,5 +25,5 @@ export const startTypingIndicatorCleanup = Effect.gen(function* () { yield* Effect.logDebug("Starting typing indicator cleanup job") // Run cleanup every 5 seconds - yield* cleanupStaleIndicators.pipe(Effect.repeat(Schedule.fixed(5000)), Effect.fork) + yield* cleanupStaleIndicators.pipe(Effect.repeat(Schedule.fixed(5000)), Effect.forkChild) }) diff --git a/apps/backend/src/services/workos-auth.ts b/apps/backend/src/services/workos-auth.ts index fb5956038..b9fdd0e5a 100644 --- a/apps/backend/src/services/workos-auth.ts +++ b/apps/backend/src/services/workos-auth.ts @@ -1,5 +1,5 @@ import { WorkOS as WorkOSNodeAPI } from "@workos-inc/node" -import { ServiceMap, Config, Effect, Redacted, Schema } from "effect" +import { ServiceMap, Config, Effect, Layer, Redacted, Schema } from "effect" export class WorkOSAuthError extends Schema.TaggedErrorClass()("WorkOSAuthError", { cause: Schema.Unknown, diff --git a/apps/backend/src/services/workos-webhook.ts b/apps/backend/src/services/workos-webhook.ts index c9ddfd5e5..de1598397 100644 --- a/apps/backend/src/services/workos-webhook.ts +++ b/apps/backend/src/services/workos-webhook.ts @@ -1,5 +1,5 @@ import * as crypto from "node:crypto" -import { ServiceMap, Config, DateTime, Duration, Effect, Schema } from "effect" +import { ServiceMap, Config, DateTime, Duration, Effect, Layer, Schema } from "effect" // Error types export class WebhookVerificationError extends Schema.TaggedErrorClass( @@ -81,11 +81,11 @@ export class WorkOSWebhookVerifier extends ServiceMap.Service => Effect.gen(function* () { - const webhookTime = DateTime.unsafeMake(timestamp) - const now = DateTime.unsafeNow() - const difference = DateTime.distanceDuration(webhookTime, now) + const webhookTime = DateTime.makeUnsafe(timestamp) + const now = DateTime.nowUnsafe() + const difference = DateTime.distance(webhookTime, now) - if (Duration.greaterThan(difference, tolerance)) { + if (Duration.isGreaterThan(difference, tolerance)) { return yield* Effect.fail( new WebhookTimestampError({ message: `Webhook timestamp is too old. Difference: ${Duration.format(difference)}, Tolerance: ${Duration.format(tolerance)}`, From 3d7453d20df7c794fc3f84aacaafe8330151c487 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 13:34:40 +0100 Subject: [PATCH 17/34] fix --- apps/backend/src/routes/internal.http.ts | 5 ++--- apps/backend/src/routes/klipy.http.ts | 3 --- apps/backend/src/services/rate-limiter.ts | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/backend/src/routes/internal.http.ts b/apps/backend/src/routes/internal.http.ts index 292b0d5a8..def567b64 100644 --- a/apps/backend/src/routes/internal.http.ts +++ b/apps/backend/src/routes/internal.http.ts @@ -22,9 +22,8 @@ export const HttpInternalLive = HttpApiBuilder.group(HazelApi, "internal", (hand const request = yield* HttpServerRequest.HttpServerRequest // Optionally verify internal secret for server-to-server auth - const internalSecret = yield* Effect.option(Config.string("INTERNAL_SECRET")).pipe( - Effect.map(Option.getOrUndefined), - ) + const internalSecretOption = yield* Config.string("INTERNAL_SECRET").pipe(Config.option) + const internalSecret = Option.getOrUndefined(internalSecretOption) if (internalSecret) { const providedSecret = request.headers["x-internal-secret"] diff --git a/apps/backend/src/routes/klipy.http.ts b/apps/backend/src/routes/klipy.http.ts index 9f394464c..8b56b5b33 100644 --- a/apps/backend/src/routes/klipy.http.ts +++ b/apps/backend/src/routes/klipy.http.ts @@ -89,9 +89,6 @@ const fetchKlipy = ( Effect.catchTag("HttpClientError", (error) => Effect.fail(new KlipyApiError({ message: `Klipy request failed: ${String(error)}` })), ), - Effect.catchTag("HttpClientError", (error) => - Effect.fail(new KlipyApiError({ message: `Klipy response error: ${String(error)}` })), - ), ) } diff --git a/apps/backend/src/services/rate-limiter.ts b/apps/backend/src/services/rate-limiter.ts index ca54eeac3..5802561c3 100644 --- a/apps/backend/src/services/rate-limiter.ts +++ b/apps/backend/src/services/rate-limiter.ts @@ -108,7 +108,7 @@ const memoryStore = new Map() export const RateLimiterMemoryLive = Layer.succeed( RateLimiter, - RateLimiter.make({ + RateLimiter.of({ consume: (key: string, limit: number, windowMs: number) => Effect.sync(() => { // Simple in-memory implementation using a Map From 455d5b9d605556d9aa44390a249449e170ee66c7 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 13:41:43 +0100 Subject: [PATCH 18/34] fux --- apps/backend/src/index.ts | 10 +++------- apps/backend/src/routes/api-v1/messages.http.ts | 3 ++- apps/web/src/lib/error-messages.ts | 1 - 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 50c0ce4db..c7903c160 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -41,7 +41,7 @@ import { import { Redis, RedisResultPersistenceLive, S3 } from "@hazel/effect-bun" import { createTracingLayer } from "@hazel/effect-bun/Telemetry" import { GitHub } from "@hazel/integrations" -import { Config, ConfigProvider, Effect, Layer, Scope, ServiceMap } from "effect" +import { Config, ConfigProvider, Effect, Layer, ServiceMap } from "effect" import { HazelApi } from "./api" import { HttpApiRoutes } from "./http" import { AttachmentPolicy } from "./policies/attachment-policy" @@ -222,7 +222,7 @@ const ServerLayer = HttpRouter.serve(AllRoutes).pipe( Layer.provide( Layer.succeed( HttpMiddleware.TracerDisabledWhen, - (request: any) => request.url === "/health" || request.method === "OPTIONS", + (request) => request.url === "/health" || request.method === "OPTIONS", ), ), Layer.provide(MainLive), @@ -247,8 +247,4 @@ const ServerLayer = HttpRouter.serve(AllRoutes).pipe( ), ) -const ServerProgram = Effect.scoped( - ServerLayer.pipe(Layer.launch) as unknown as Effect.Effect, -) - -BunRuntime.runMain(ServerProgram) +ServerLayer.pipe(Layer.launch).pipe(BunRuntime.runMain) diff --git a/apps/backend/src/routes/api-v1/messages.http.ts b/apps/backend/src/routes/api-v1/messages.http.ts index a3e1159c7..c29c405d4 100644 --- a/apps/backend/src/routes/api-v1/messages.http.ts +++ b/apps/backend/src/routes/api-v1/messages.http.ts @@ -91,7 +91,8 @@ const createBotUserContext = (bot: { userId: typeof import("@hazel/schema").User const withHttpScopes = ( scopes: ReadonlyArray, make: Effect.Effect, -): Effect.Effect => Effect.provideService(make, CurrentRpcScopes, scopes as any) +): Effect.Effect => + Effect.provideService(make, CurrentRpcScopes, scopes) as Effect.Effect export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messages", (handlers) => Effect.gen(function* () { diff --git a/apps/web/src/lib/error-messages.ts b/apps/web/src/lib/error-messages.ts index 84c4324eb..c5e07598c 100644 --- a/apps/web/src/lib/error-messages.ts +++ b/apps/web/src/lib/error-messages.ts @@ -1,4 +1,3 @@ -import { Schema } from "effect" import type { HttpClientError } from "effect/unstable/http" import { RpcClientError } from "effect/unstable/rpc" import { From 6cd37bcf1351da0c4f3c93d677e3bd4a3d214b9f Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 13:56:49 +0100 Subject: [PATCH 19/34] fix --- apps/backend/package.json | 2 +- apps/backend/src/index.ts | 3 +- apps/backend/src/lib/create-transactionId.ts | 5 +- .../src/routes/api-v1/messages.http.ts | 41 +- apps/backend/src/routes/auth.http.ts | 24 +- apps/backend/src/routes/chat-sync.http.ts | 4 +- apps/backend/src/routes/integrations.http.ts | 14 +- apps/backend/src/routes/webhooks.http.ts | 4 +- apps/backend/src/rpc/handlers/bots.ts | 15 +- .../src/rpc/handlers/channel-members.ts | 56 +- .../src/rpc/handlers/channel-sections.ts | 6 +- apps/backend/src/rpc/handlers/channels.ts | 38 +- .../backend/src/rpc/handlers/custom-emojis.ts | 14 +- .../src/rpc/handlers/message-reactions.ts | 28 +- apps/backend/src/rpc/handlers/messages.ts | 20 +- .../backend/src/rpc/handlers/notifications.ts | 21 +- .../src/rpc/handlers/organization-members.ts | 12 +- .../backend/src/rpc/handlers/organizations.ts | 29 +- .../src/rpc/handlers/pinned-messages.ts | 14 +- .../src/services/bot-gateway-service.ts | 20 +- .../chat-sync/chat-sync-core-worker.ts | 4 +- .../chat-sync/chat-sync-provider-registry.ts | 4 +- apps/bot-gateway/src/index.test.ts | 15 +- apps/bot-gateway/src/index.ts | 6 +- .../src/workflows/cleanup-uploads-handler.ts | 5 +- .../workflows/github-installation-handler.ts | 12 +- .../src/workflows/github-webhook-handler.ts | 38 +- .../workflows/message-notification-handler.ts | 31 +- .../src/workflows/rss-feed-poll-handler.ts | 12 +- .../src/workflows/thread-naming-handler.ts | 86 +- apps/docs/src/routeTree.gen.ts | 104 +- .../src/cache/access-context-service.ts | 20 +- apps/electric-proxy/src/index.ts | 10 +- .../src/handlers/link-preview.ts | 6 +- .../link-preview-worker/src/handlers/tweet.ts | 4 +- apps/link-preview-worker/src/index.ts | 11 +- .../chat/slate-editor/mention-element.tsx | 3 +- apps/web/src/db/actions.ts | 4 +- apps/web/src/lib/auth-token.ts | 9 +- apps/web/src/routeTree.gen.ts | 2488 ++++++++--------- bots/hazel-bot/src/agent-loop.ts | 18 +- bots/hazel-bot/src/degeneration-detector.ts | 5 +- bots/hazel-bot/src/handler.ts | 4 +- bots/hazel-bot/src/tools/toolkit.ts | 4 +- bots/linear-bot/src/index.ts | 4 +- libs/bot-sdk/src/hazel-bot-sdk.ts | 78 +- libs/bot-sdk/src/log-config.ts | 5 +- package.json | 2 +- packages/domain/src/errors.ts | 6 +- 49 files changed, 1697 insertions(+), 1671 deletions(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index 633ac5119..1c8ef8de5 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -48,4 +48,4 @@ "drizzle-kit": "^0.31.8", "typescript": "^5.9.3" } -} \ No newline at end of file +} diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index c7903c160..7eaa81a34 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -247,4 +247,5 @@ const ServerLayer = HttpRouter.serve(AllRoutes).pipe( ), ) -ServerLayer.pipe(Layer.launch).pipe(BunRuntime.runMain) +// TODO: Layer has leaked dependencies — fix service layer wiring so this cast is unnecessary +ServerLayer.pipe(Layer.launch as never, BunRuntime.runMain) diff --git a/apps/backend/src/lib/create-transactionId.ts b/apps/backend/src/lib/create-transactionId.ts index 65f07139f..8aed731d2 100644 --- a/apps/backend/src/lib/create-transactionId.ts +++ b/apps/backend/src/lib/create-transactionId.ts @@ -38,10 +38,7 @@ export const generateTransactionId = Effect.fn("generateTransactionId")(function (e): e is Database.DatabaseError => Predicate.isTagged(e, "DatabaseError"), (err) => Effect.die(`Database error generating transaction ID: ${err}`), ), - Effect.catchIf( - SchemaIssue.isIssue, - (err) => Effect.die(`Failed to parse transaction ID: ${err}`), - ), + Effect.catchIf(SchemaIssue.isIssue, (err) => Effect.die(`Failed to parse transaction ID: ${err}`)), ) return result diff --git a/apps/backend/src/routes/api-v1/messages.http.ts b/apps/backend/src/routes/api-v1/messages.http.ts index c29c405d4..1e0bac277 100644 --- a/apps/backend/src/routes/api-v1/messages.http.ts +++ b/apps/backend/src/routes/api-v1/messages.http.ts @@ -91,8 +91,7 @@ const createBotUserContext = (bot: { userId: typeof import("@hazel/schema").User const withHttpScopes = ( scopes: ReadonlyArray, make: Effect.Effect, -): Effect.Effect => - Effect.provideService(make, CurrentRpcScopes, scopes) as Effect.Effect +): Effect.Effect => Effect.provideService(make, CurrentRpcScopes, scopes) as Effect.Effect export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messages", (handlers) => Effect.gen(function* () { @@ -130,9 +129,9 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag const effectiveLimit = limit ?? 25 // First, check if user can read this channel (policy authorization) - yield* messagePolicy.canRead(channel_id).pipe( - Effect.provideService(CurrentUser.Context, currentUser), - ) + yield* messagePolicy + .canRead(channel_id) + .pipe(Effect.provideService(CurrentUser.Context, currentUser)) // Resolve cursor IDs to stable cursor tuples. let cursorBefore: @@ -227,14 +226,16 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag .transaction( Effect.gen(function* () { yield* messagePolicy.canCreate(rest.channelId) - const createdMessage = yield* messageRepo.insert({ - ...rest, - embeds: embeds ?? null, - replyToMessageId: replyToMessageId ?? null, - threadChannelId: threadChannelId ?? null, - authorId: bot.userId, - deletedAt: null, - }).pipe(Effect.map((res) => res[0]!)) + const createdMessage = yield* messageRepo + .insert({ + ...rest, + embeds: embeds ?? null, + replyToMessageId: replyToMessageId ?? null, + threadChannelId: threadChannelId ?? null, + authorId: bot.userId, + deletedAt: null, + }) + .pipe(Effect.map((res) => res[0]!)) // Link attachments if provided if (attachmentIds && attachmentIds.length > 0) { @@ -492,12 +493,14 @@ export const HttpMessagesApiLive = HttpApiBuilder.group(HazelApi, "api-v1-messag // Otherwise, create a new reaction yield* messageReactionPolicy.canCreate(messageId) - const createdReaction = yield* messageReactionRepo.insert({ - messageId, - channelId, - emoji, - userId: bot.userId, - }).pipe(Effect.map((res) => res[0]!)) + const createdReaction = yield* messageReactionRepo + .insert({ + messageId, + channelId, + emoji, + userId: bot.userId, + }) + .pipe(Effect.map((res) => res[0]!)) yield* outboxRepo.insert({ eventType: "reaction_created", diff --git a/apps/backend/src/routes/auth.http.ts b/apps/backend/src/routes/auth.http.ts index c2fa018de..5b6f5a0d9 100644 --- a/apps/backend/src/routes/auth.http.ts +++ b/apps/backend/src/routes/auth.http.ts @@ -81,7 +81,9 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => }) }).pipe( Effect.catchTag("ConfigError", (err) => - Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + Effect.fail( + new InternalServerError({ message: "Missing configuration", detail: String(err) }), + ), ), ), ) @@ -115,7 +117,9 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => }) }).pipe( Effect.catchTag("ConfigError", (err) => - Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + Effect.fail( + new InternalServerError({ message: "Missing configuration", detail: String(err) }), + ), ), ), ) @@ -134,7 +138,9 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => }) }).pipe( Effect.catchTag("ConfigError", (err) => - Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + Effect.fail( + new InternalServerError({ message: "Missing configuration", detail: String(err) }), + ), ), ), ) @@ -202,7 +208,9 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => }) }).pipe( Effect.catchTag("ConfigError", (err) => - Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + Effect.fail( + new InternalServerError({ message: "Missing configuration", detail: String(err) }), + ), ), ), ) @@ -310,7 +318,9 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => } }).pipe( Effect.catchTag("ConfigError", (err) => - Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + Effect.fail( + new InternalServerError({ message: "Missing configuration", detail: String(err) }), + ), ), ), ) @@ -349,7 +359,9 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => } }).pipe( Effect.catchTag("ConfigError", (err) => - Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + Effect.fail( + new InternalServerError({ message: "Missing configuration", detail: String(err) }), + ), ), ), ), diff --git a/apps/backend/src/routes/chat-sync.http.ts b/apps/backend/src/routes/chat-sync.http.ts index e1cad8d6f..e2490d453 100644 --- a/apps/backend/src/routes/chat-sync.http.ts +++ b/apps/backend/src/routes/chat-sync.http.ts @@ -173,7 +173,9 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han ), ), Effect.catchTag("SchemaError", (error) => - Effect.fail(toInternalServerError("Schema error while deleting sync connection", error)), + Effect.fail( + toInternalServerError("Schema error while deleting sync connection", error), + ), ), ), ) diff --git a/apps/backend/src/routes/integrations.http.ts b/apps/backend/src/routes/integrations.http.ts index 6da0bd544..75c1bde64 100644 --- a/apps/backend/src/routes/integrations.http.ts +++ b/apps/backend/src/routes/integrations.http.ts @@ -252,7 +252,8 @@ const makeOAuthSessionCookie = ( Effect.try({ try: () => Cookies.makeCookieUnsafe(name, value, { - domain: options.cookieDomain, path: "/", + domain: options.cookieDomain, + path: "/", httpOnly: true, secure: options.secure, sameSite: "lax", @@ -267,7 +268,8 @@ const makeOAuthSessionCookie = ( const expireOAuthSessionCookie = (name: string, options: { cookieDomain: string; secure: boolean }) => HttpServerResponse.expireCookie(name, { - domain: options.cookieDomain, path: "/", + domain: options.cookieDomain, + path: "/", httpOnly: true, secure: options.secure, sameSite: "lax", @@ -1204,7 +1206,9 @@ export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations" .handle("getOAuthUrl", ({ params, query }) => handleGetOAuthUrl(params, query).pipe( Effect.catchTag("ConfigError", (err) => - Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + Effect.fail( + new InternalServerError({ message: "Missing configuration", detail: String(err) }), + ), ), ), ) @@ -1227,7 +1231,9 @@ export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations" ), ), Effect.catchTag("ConfigError", (err) => - Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + Effect.fail( + new InternalServerError({ message: "Missing configuration", detail: String(err) }), + ), ), ), ) diff --git a/apps/backend/src/routes/webhooks.http.ts b/apps/backend/src/routes/webhooks.http.ts index 144ad5ac9..e2d542406 100644 --- a/apps/backend/src/routes/webhooks.http.ts +++ b/apps/backend/src/routes/webhooks.http.ts @@ -174,7 +174,9 @@ export const HttpWebhookLive = HttpApiBuilder.group(HazelApi, "webhooks", (handl }) }).pipe( Effect.catchTag("ConfigError", (err) => - Effect.fail(new InternalServerError({ message: "Missing configuration", detail: String(err) })), + Effect.fail( + new InternalServerError({ message: "Missing configuration", detail: String(err) }), + ), ), ), ), diff --git a/apps/backend/src/rpc/handlers/bots.ts b/apps/backend/src/rpc/handlers/bots.ts index 5713716e2..a6d32756b 100644 --- a/apps/backend/src/rpc/handlers/bots.ts +++ b/apps/backend/src/rpc/handlers/bots.ts @@ -128,10 +128,7 @@ export const BotRpcLive = BotRpcs.toLayer( }), ) - yield* channelAccessSync.syncUserInOrganization( - botUserId, - organizationId, - ) + yield* channelAccessSync.syncUserInOrganization(botUserId, organizationId) const txid = yield* generateTransactionId() @@ -448,10 +445,7 @@ export const BotRpcLive = BotRpcs.toLayer( }), ) - yield* channelAccessSync.syncUserInOrganization( - bot.userId, - organizationId, - ) + yield* channelAccessSync.syncUserInOrganization(bot.userId, organizationId) // Increment install count yield* botRepo.incrementInstallCount(botId) @@ -608,10 +602,7 @@ export const BotRpcLive = BotRpcs.toLayer( }), ) - yield* channelAccessSync.syncUserInOrganization( - bot.userId, - organizationId, - ) + yield* channelAccessSync.syncUserInOrganization(bot.userId, organizationId) // Increment install count yield* botRepo.incrementInstallCount(botId) diff --git a/apps/backend/src/rpc/handlers/channel-members.ts b/apps/backend/src/rpc/handlers/channel-members.ts index 8d90d7768..5a62f8d9b 100644 --- a/apps/backend/src/rpc/handlers/channel-members.ts +++ b/apps/backend/src/rpc/handlers/channel-members.ts @@ -27,17 +27,19 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( const user = yield* CurrentUser.Context yield* channelMemberPolicy.canCreate(payload.channelId) - const createdChannelMember = yield* channelMemberRepo.insert({ - channelId: payload.channelId, - userId: user.id, - isHidden: false, - isMuted: false, - isFavorite: false, - lastSeenMessageId: null, - notificationCount: 0, - joinedAt: new Date(), - deletedAt: null, - }).pipe(Effect.map((res) => res[0]!)) + const createdChannelMember = yield* channelMemberRepo + .insert({ + channelId: payload.channelId, + userId: user.id, + isHidden: false, + isMuted: false, + isFavorite: false, + lastSeenMessageId: null, + notificationCount: 0, + joinedAt: new Date(), + deletedAt: null, + }) + .pipe(Effect.map((res) => res[0]!)) const channelOption = yield* channelRepo.findById(payload.channelId) if (Option.isSome(channelOption)) { @@ -91,9 +93,9 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( "channelMember.delete": ({ id }) => Effect.gen(function* () { - const deletedMemberOption = yield* channelMemberRepo.findById(id).pipe( - withRemapDbErrors("ChannelMember", "select"), - ) + const deletedMemberOption = yield* channelMemberRepo + .findById(id) + .pipe(withRemapDbErrors("ChannelMember", "select")) const response = yield* db .transaction( Effect.gen(function* () { @@ -101,9 +103,9 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( yield* channelMemberRepo.deleteById(id) if (Option.isSome(deletedMemberOption)) { - const channelOption = yield* channelRepo.findById( - deletedMemberOption.value.channelId, - ).pipe(withRemapDbErrors("Channel", "select")) + const channelOption = yield* channelRepo + .findById(deletedMemberOption.value.channelId) + .pipe(withRemapDbErrors("Channel", "select")) if (Option.isSome(channelOption)) { yield* channelAccessSync.syncUserInOrganization( deletedMemberOption.value.userId, @@ -144,22 +146,20 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( // Find the channel member record for this user and channel yield* channelMemberPolicy.canRead(channelId) - const memberOption = yield* channelMemberRepo.findByChannelAndUser( - channelId, - user.id, - ).pipe(withRemapDbErrors("ChannelMember", "select")) + const memberOption = yield* channelMemberRepo + .findByChannelAndUser(channelId, user.id) + .pipe(withRemapDbErrors("ChannelMember", "select")) // Get channel to find organizationId - const channelOption = yield* channelRepo.findById(channelId).pipe( - withRemapDbErrors("Channel", "select"), - ) + const channelOption = yield* channelRepo + .findById(channelId) + .pipe(withRemapDbErrors("Channel", "select")) // Get organization member for notification deletion const orgMemberOption = Option.isSome(channelOption) - ? yield* organizationMemberRepo.findByOrgAndUser( - channelOption.value.organizationId, - user.id, - ).pipe(withRemapDbErrors("OrganizationMember", "select")) + ? yield* organizationMemberRepo + .findByOrgAndUser(channelOption.value.organizationId, user.id) + .pipe(withRemapDbErrors("OrganizationMember", "select")) : Option.none() // Wrap the update and transaction ID generation in a single transaction diff --git a/apps/backend/src/rpc/handlers/channel-sections.ts b/apps/backend/src/rpc/handlers/channel-sections.ts index 344b5f5e8..f277c9b9a 100644 --- a/apps/backend/src/rpc/handlers/channel-sections.ts +++ b/apps/backend/src/rpc/handlers/channel-sections.ts @@ -48,9 +48,9 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( : { ...payload, order, deletedAt: null } yield* channelSectionPolicy.canCreate(payload.organizationId) - const createdSection = yield* channelSectionRepo.insert( - insertData as typeof payload & { order: number; deletedAt: null }, - ).pipe(Effect.map((res) => res[0]!)) + const createdSection = yield* channelSectionRepo + .insert(insertData as typeof payload & { order: number; deletedAt: null }) + .pipe(Effect.map((res) => res[0]!)) const txid = yield* generateTransactionId() diff --git a/apps/backend/src/rpc/handlers/channels.ts b/apps/backend/src/rpc/handlers/channels.ts index 7575c7997..5be85d373 100644 --- a/apps/backend/src/rpc/handlers/channels.ts +++ b/apps/backend/src/rpc/handlers/channels.ts @@ -51,9 +51,9 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( : { ...payload, deletedAt: null } yield* channelPolicy.canCreate(payload.organizationId) - const createdChannel = yield* channelRepo.insert( - insertData as typeof payload & { deletedAt: null }, - ).pipe(Effect.map((res) => res[0]!)) + const createdChannel = yield* channelRepo + .insert(insertData as typeof payload & { deletedAt: null }) + .pipe(Effect.map((res) => res[0]!)) yield* channelMemberRepo.insert({ channelId: createdChannel.id, @@ -151,9 +151,9 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( "channel.delete": ({ id }) => Effect.gen(function* () { - const existingChannel = yield* channelRepo.findById(id).pipe( - withRemapDbErrors("Channel", "select"), - ) + const existingChannel = yield* channelRepo + .findById(id) + .pipe(withRemapDbErrors("Channel", "select")) const response = yield* db .transaction( Effect.gen(function* () { @@ -237,15 +237,17 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( // Create channel yield* channelPolicy.canCreate(OrganizationId.makeUnsafe(payload.organizationId)) - const createdChannel = yield* channelRepo.insert({ - name: channelName || "Group Channel", - icon: null, - type: payload.type, - organizationId: OrganizationId.makeUnsafe(payload.organizationId), - parentChannelId: null, - sectionId: null, - deletedAt: null, - }).pipe(Effect.map((res) => res[0]!)) + const createdChannel = yield* channelRepo + .insert({ + name: channelName || "Group Channel", + icon: null, + type: payload.type, + organizationId: OrganizationId.makeUnsafe(payload.organizationId), + parentChannelId: null, + sectionId: null, + deletedAt: null, + }) + .pipe(Effect.map((res) => res[0]!)) // Add creator as member yield* channelMemberRepo.insert({ @@ -392,9 +394,9 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( } yield* channelPolicy.canCreate(organizationId) - const createdChannel = yield* channelRepo.insert(insertData).pipe( - Effect.map((res) => res[0]!), - ) + const createdChannel = yield* channelRepo + .insert(insertData) + .pipe(Effect.map((res) => res[0]!)) // 3. Add creator as member yield* channelMemberRepo.insert({ diff --git a/apps/backend/src/rpc/handlers/custom-emojis.ts b/apps/backend/src/rpc/handlers/custom-emojis.ts index 1abf6e641..08ef50b5c 100644 --- a/apps/backend/src/rpc/handlers/custom-emojis.ts +++ b/apps/backend/src/rpc/handlers/custom-emojis.ts @@ -55,12 +55,14 @@ export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( } yield* customEmojiPolicy.canCreate(payload.organizationId) - const created = yield* customEmojiRepo.insert({ - organizationId: payload.organizationId, - name: payload.name, - imageUrl: payload.imageUrl, - createdBy: user.id, - }).pipe(Effect.map((res) => res[0]!)) + const created = yield* customEmojiRepo + .insert({ + organizationId: payload.organizationId, + name: payload.name, + imageUrl: payload.imageUrl, + createdBy: user.id, + }) + .pipe(Effect.map((res) => res[0]!)) const txid = yield* generateTransactionId() diff --git a/apps/backend/src/rpc/handlers/message-reactions.ts b/apps/backend/src/rpc/handlers/message-reactions.ts index 7841f50d8..a4986113a 100644 --- a/apps/backend/src/rpc/handlers/message-reactions.ts +++ b/apps/backend/src/rpc/handlers/message-reactions.ts @@ -69,13 +69,15 @@ export const MessageReactionRpcLive = MessageReactionRpcs.toLayer( yield* messageReactionPolicy.canCreate(messageId) const conversationId = yield* connectConversationService.getConversationIdForChannel(channelId) - const createdMessageReaction = yield* messageReactionRepo.insert({ - messageId, - channelId, - conversationId, - emoji, - userId: user.id, - }).pipe(Effect.map((res) => res[0]!)) + const createdMessageReaction = yield* messageReactionRepo + .insert({ + messageId, + channelId, + conversationId, + emoji, + userId: user.id, + }) + .pipe(Effect.map((res) => res[0]!)) yield* outboxRepo.insert({ eventType: "reaction_created", @@ -115,11 +117,13 @@ export const MessageReactionRpcLive = MessageReactionRpcs.toLayer( yield* connectConversationService.getConversationIdForChannel( payload.channelId, ) - const createdMessageReaction = yield* messageReactionRepo.insert({ - ...payload, - conversationId, - userId: user.id, - }).pipe(Effect.map((res) => res[0]!)) + const createdMessageReaction = yield* messageReactionRepo + .insert({ + ...payload, + conversationId, + userId: user.id, + }) + .pipe(Effect.map((res) => res[0]!)) yield* outboxRepo.insert({ eventType: "reaction_created", diff --git a/apps/backend/src/rpc/handlers/messages.ts b/apps/backend/src/rpc/handlers/messages.ts index 0a18fa22c..bc0ea65da 100644 --- a/apps/backend/src/rpc/handlers/messages.ts +++ b/apps/backend/src/rpc/handlers/messages.ts @@ -51,12 +51,14 @@ export const MessageRpcLive = MessageRpcs.toLayer( yield* connectConversationService.getConversationIdForChannel( messageData.channelId, ) - const createdMessage = yield* messageRepo.insert({ - ...messageData, - conversationId, - authorId: user.id, - deletedAt: null, - }).pipe(Effect.map((res) => res[0]!)) + const createdMessage = yield* messageRepo + .insert({ + ...messageData, + conversationId, + authorId: user.id, + deletedAt: null, + }) + .pipe(Effect.map((res) => res[0]!)) // Update attachments with messageId if provided if (attachmentIds && attachmentIds.length > 0) { @@ -156,9 +158,9 @@ export const MessageRpcLive = MessageRpcs.toLayer( "message.delete": ({ id }) => Effect.gen(function* () { const user = yield* CurrentUser.Context - const existingMessage = yield* messageRepo.findById(id).pipe( - withRemapDbErrors("Message", "select"), - ) + const existingMessage = yield* messageRepo + .findById(id) + .pipe(withRemapDbErrors("Message", "select")) // Check rate limit before processing yield* checkMessageRateLimit(user.id) diff --git a/apps/backend/src/rpc/handlers/notifications.ts b/apps/backend/src/rpc/handlers/notifications.ts index 8e7ccbb73..910276580 100644 --- a/apps/backend/src/rpc/handlers/notifications.ts +++ b/apps/backend/src/rpc/handlers/notifications.ts @@ -33,9 +33,11 @@ export const NotificationRpcLive = NotificationRpcs.toLayer( .transaction( Effect.gen(function* () { yield* notificationPolicy.canCreate(payload.memberId) - const createdNotification = yield* notificationRepo.insert({ - ...payload, - }).pipe(Effect.map((res) => res[0]!)) + const createdNotification = yield* notificationRepo + .insert({ + ...payload, + }) + .pipe(Effect.map((res) => res[0]!)) const txid = yield* generateTransactionId() @@ -92,9 +94,9 @@ export const NotificationRpcLive = NotificationRpcs.toLayer( const user = yield* CurrentUser.Context // Get the channel to find the organization (system operation) - const channelOption = yield* channelRepo.findById(channelId).pipe( - withRemapDbErrors("Channel", "select"), - ) + const channelOption = yield* channelRepo + .findById(channelId) + .pipe(withRemapDbErrors("Channel", "select")) if (Option.isNone(channelOption)) { return yield* Effect.fail( @@ -108,10 +110,9 @@ export const NotificationRpcLive = NotificationRpcs.toLayer( const channel = channelOption.value // Get the organization member for this user (system operation) - const memberOption = yield* organizationMemberRepo.findByOrgAndUser( - channel.organizationId, - user.id, - ).pipe(withRemapDbErrors("OrganizationMember", "select")) + const memberOption = yield* organizationMemberRepo + .findByOrgAndUser(channel.organizationId, user.id) + .pipe(withRemapDbErrors("OrganizationMember", "select")) if (Option.isNone(memberOption)) { return yield* Effect.fail( diff --git a/apps/backend/src/rpc/handlers/organization-members.ts b/apps/backend/src/rpc/handlers/organization-members.ts index 0a7b4bcf0..6e8b50fa3 100644 --- a/apps/backend/src/rpc/handlers/organization-members.ts +++ b/apps/backend/src/rpc/handlers/organization-members.ts @@ -35,11 +35,13 @@ export const OrganizationMemberRpcLive = OrganizationMemberRpcs.toLayer( const user = yield* CurrentUser.Context yield* organizationMemberPolicy.canCreate(payload.organizationId) - const createdOrganizationMember = yield* organizationMemberRepo.insert({ - ...payload, - userId: user.id, - deletedAt: null, - }).pipe(Effect.map((res) => res[0]!)) + const createdOrganizationMember = yield* organizationMemberRepo + .insert({ + ...payload, + userId: user.id, + deletedAt: null, + }) + .pipe(Effect.map((res) => res[0]!)) yield* channelAccessSync.syncUserInOrganization( createdOrganizationMember.userId, diff --git a/apps/backend/src/rpc/handlers/organizations.ts b/apps/backend/src/rpc/handlers/organizations.ts index 90ba4da92..0c2fb62e7 100644 --- a/apps/backend/src/rpc/handlers/organizations.ts +++ b/apps/backend/src/rpc/handlers/organizations.ts @@ -40,10 +40,12 @@ const handleOrganizationDbErrors = ( > => { return effect.pipe( Effect.catchIf( - (e): e is Extract => - Predicate.isTagged(e, "DatabaseError"), + (e): e is Extract => Predicate.isTagged(e, "DatabaseError"), (err) => { - const dbErr = err as unknown as { type: string; cause: { constraint_name?: string; detail?: string } } + const dbErr = err as unknown as { + type: string + cause: { constraint_name?: string; detail?: string } + } // Check if it's a unique violation on the slug column if ( dbErr.type === "unique_violation" && @@ -70,8 +72,7 @@ const handleOrganizationDbErrors = ( }, ), Effect.catchIf( - (e): e is Extract => - Predicate.isTagged(e, "SchemaError"), + (e): e is Extract => Predicate.isTagged(e, "SchemaError"), (err) => Effect.fail( new InternalServerError({ @@ -163,14 +164,16 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( // Create organization in local database first yield* organizationPolicy.canCreate() - const createdOrganization = yield* organizationRepo.insert({ - name: payload.name, - slug: payload.slug, - logoUrl: payload.logoUrl, - settings: payload.settings, - isPublic: false, - deletedAt: null, - }).pipe(Effect.map((res) => res[0]!)) + const createdOrganization = yield* organizationRepo + .insert({ + name: payload.name, + slug: payload.slug, + logoUrl: payload.logoUrl, + settings: payload.settings, + isPublic: false, + deletedAt: null, + }) + .pipe(Effect.map((res) => res[0]!)) // Create organization in WorkOS using our DB ID as externalId const workosOrg = yield* workos diff --git a/apps/backend/src/rpc/handlers/pinned-messages.ts b/apps/backend/src/rpc/handlers/pinned-messages.ts index c02c840af..a05601c80 100644 --- a/apps/backend/src/rpc/handlers/pinned-messages.ts +++ b/apps/backend/src/rpc/handlers/pinned-messages.ts @@ -35,12 +35,14 @@ export const PinnedMessageRpcLive = PinnedMessageRpcs.toLayer( const user = yield* CurrentUser.Context yield* pinnedMessagePolicy.canCreate(payload.channelId) - const createdPinnedMessage = yield* pinnedMessageRepo.insert({ - channelId: payload.channelId, - messageId: payload.messageId, - pinnedBy: user.id, - pinnedAt: new Date(), - }).pipe(Effect.map((res) => res[0]!)) + const createdPinnedMessage = yield* pinnedMessageRepo + .insert({ + channelId: payload.channelId, + messageId: payload.messageId, + pinnedBy: user.id, + pinnedAt: new Date(), + }) + .pipe(Effect.map((res) => res[0]!)) const txid = yield* generateTransactionId() diff --git a/apps/backend/src/services/bot-gateway-service.ts b/apps/backend/src/services/bot-gateway-service.ts index b886cb1a4..3ac908bb9 100644 --- a/apps/backend/src/services/bot-gateway-service.ts +++ b/apps/backend/src/services/bot-gateway-service.ts @@ -190,8 +190,11 @@ export class BotGatewayService extends ServiceMap.Service()(" return } - const eventTimestamp = - message.updatedAt ? toEpochMs(message.updatedAt) : message.createdAt ? toEpochMs(message.createdAt) : Date.now() + const eventTimestamp = message.updatedAt + ? toEpochMs(message.updatedAt) + : message.createdAt + ? toEpochMs(message.createdAt) + : Date.now() yield* publishToInstalledBots(organizationId, () => ({ schemaVersion: 1, @@ -211,8 +214,11 @@ export class BotGatewayService extends ServiceMap.Service()(" eventType: "channel.create" | "channel.update" | "channel.delete", channel: Schema.Schema.Type, ) { - const eventTimestamp = - channel.updatedAt ? toEpochMs(channel.updatedAt) : channel.createdAt ? toEpochMs(channel.createdAt) : Date.now() + const eventTimestamp = channel.updatedAt + ? toEpochMs(channel.updatedAt) + : channel.createdAt + ? toEpochMs(channel.createdAt) + : Date.now() yield* publishToInstalledBots(channel.organizationId, () => ({ schemaVersion: 1, @@ -237,7 +243,11 @@ export class BotGatewayService extends ServiceMap.Service()(" return } - const eventTimestamp = member.createdAt ? toEpochMs(member.createdAt) : member.joinedAt ? toEpochMs(member.joinedAt) : Date.now() + const eventTimestamp = member.createdAt + ? toEpochMs(member.createdAt) + : member.joinedAt + ? toEpochMs(member.joinedAt) + : Date.now() yield* publishToInstalledBots(organizationId, () => ({ schemaVersion: 1, diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index 611260278..31ee34c3c 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -1,4 +1,5 @@ import { createHash } from "node:crypto" +import type { DiscordSyncWorker as DiscordSyncWorkerType } from "./discord-sync-worker" import { and, asc, Database, eq, isNull, schema } from "@hazel/db" import { ChannelRepo, @@ -168,7 +169,8 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() const channelAccessSyncService = yield* ChannelAccessSyncService const providerRegistry = yield* ChatSyncProviderRegistry const discordApiClient = yield* Discord.DiscordApiClient - const discordSyncWorker = yield* DiscordSyncWorker + const { DiscordSyncWorker } = yield* Effect.promise(() => import("./discord-sync-worker")) + const discordSyncWorker = yield* DiscordSyncWorker as typeof DiscordSyncWorkerType const payloadHash = (value: unknown): string => createHash("sha256").update(JSON.stringify(value)).digest("hex") diff --git a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts index 16de84c69..f6558b916 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts @@ -438,7 +438,5 @@ export class ChatSyncProviderRegistry extends ServiceMap.Service { const result = await Effect.runPromise( Effect.scoped( Layer.build( - instrumentStartupLayer(Layer.effectDiscard(Effect.fail(new Error("tracer unavailable"))), { - dependency: "tracer", - startMessage: "tracer start", - successMessage: "tracer ok", - failureMessage: "tracer failed", - }), + instrumentStartupLayer( + Layer.effectDiscard(Effect.fail(new Error("tracer unavailable"))), + { + dependency: "tracer", + startMessage: "tracer start", + successMessage: "tracer ok", + failureMessage: "tracer failed", + }, + ), ).pipe(Effect.result), ), ) diff --git a/apps/bot-gateway/src/index.ts b/apps/bot-gateway/src/index.ts index 2b0688edd..18e29f4ea 100644 --- a/apps/bot-gateway/src/index.ts +++ b/apps/bot-gateway/src/index.ts @@ -820,7 +820,11 @@ export const instrumentStartupLayer = ( Layer.catchCause((cause) => Layer.effectDiscard( Effect.fail( - makeStartupError(options.dependency, options.failureMessage, Cause.squash(cause)), + makeStartupError( + options.dependency, + options.failureMessage, + Cause.squash(cause), + ), ), ), ), diff --git a/apps/cluster/src/workflows/cleanup-uploads-handler.ts b/apps/cluster/src/workflows/cleanup-uploads-handler.ts index 1669db2a7..15e20bc1e 100644 --- a/apps/cluster/src/workflows/cleanup-uploads-handler.ts +++ b/apps/cluster/src/workflows/cleanup-uploads-handler.ts @@ -7,7 +7,10 @@ import { Effect } from "effect" const DEFAULT_MAX_AGE_MINUTES = 10 export const CleanupUploadsWorkflowLayer = Cluster.CleanupUploadsWorkflow.toLayer( - Effect.fn("workflow.CleanupUploads")(function* (payload: Cluster.CleanupUploadsWorkflowPayload, _executionId: string) { + Effect.fn("workflow.CleanupUploads")(function* ( + payload: Cluster.CleanupUploadsWorkflowPayload, + _executionId: string, + ) { const maxAgeMinutes = payload.maxAgeMinutes ?? DEFAULT_MAX_AGE_MINUTES yield* Effect.annotateCurrentSpan("workflow.max_age_minutes", maxAgeMinutes) diff --git a/apps/cluster/src/workflows/github-installation-handler.ts b/apps/cluster/src/workflows/github-installation-handler.ts index ebc559e53..0894622a6 100644 --- a/apps/cluster/src/workflows/github-installation-handler.ts +++ b/apps/cluster/src/workflows/github-installation-handler.ts @@ -4,7 +4,10 @@ import { Cluster } from "@hazel/domain" import { Effect } from "effect" export const GitHubInstallationWorkflowLayer = Cluster.GitHubInstallationWorkflow.toLayer( - Effect.fn("workflow.GitHubInstallation")(function* (payload: Cluster.GitHubInstallationWorkflowPayload, _executionId: string) { + Effect.fn("workflow.GitHubInstallation")(function* ( + payload: Cluster.GitHubInstallationWorkflowPayload, + _executionId: string, + ) { yield* Effect.annotateCurrentSpan("workflow.action", payload.action) yield* Effect.annotateCurrentSpan("workflow.installation_id", payload.installationId) yield* Effect.annotateCurrentSpan("workflow.account_login", payload.accountLogin) @@ -30,7 +33,9 @@ export const GitHubInstallationWorkflowLayer = Cluster.GitHubInstallationWorkflo execute: Effect.gen(function* () { const db = yield* Database.Database - yield* Effect.logDebug(`Querying connection for installation ID ${payload.installationId}`) + yield* Effect.logDebug( + `Querying connection for installation ID ${payload.installationId}`, + ) // Query for a connection with matching installationId in metadata const connections = yield* db @@ -40,7 +45,8 @@ export const GitHubInstallationWorkflowLayer = Cluster.GitHubInstallationWorkflo id: schema.integrationConnectionsTable.id, organizationId: schema.integrationConnectionsTable.organizationId, status: schema.integrationConnectionsTable.status, - externalAccountName: schema.integrationConnectionsTable.externalAccountName, + externalAccountName: + schema.integrationConnectionsTable.externalAccountName, }) .from(schema.integrationConnectionsTable) .where( diff --git a/apps/cluster/src/workflows/github-webhook-handler.ts b/apps/cluster/src/workflows/github-webhook-handler.ts index bca37c12e..756d30696 100644 --- a/apps/cluster/src/workflows/github-webhook-handler.ts +++ b/apps/cluster/src/workflows/github-webhook-handler.ts @@ -7,7 +7,10 @@ import { Effect, Option, Schema } from "effect" import { BotUserService } from "../services/bot-user-service.ts" export const GitHubWebhookWorkflowLayer = Cluster.GitHubWebhookWorkflow.toLayer( - Effect.fn("workflow.GitHubWebhook")(function* (payload: Cluster.GitHubWebhookWorkflowPayload, _executionId: string) { + Effect.fn("workflow.GitHubWebhook")(function* ( + payload: Cluster.GitHubWebhookWorkflowPayload, + _executionId: string, + ) { yield* Effect.annotateCurrentSpan("workflow.event_type", payload.eventType) yield* Effect.annotateCurrentSpan("workflow.repository", payload.repositoryFullName) yield* Effect.annotateCurrentSpan("workflow.repository_id", payload.repositoryId) @@ -46,7 +49,10 @@ export const GitHubWebhookWorkflowLayer = Cluster.GitHubWebhookWorkflow.toLayer( .from(schema.githubSubscriptionsTable) .where( and( - eq(schema.githubSubscriptionsTable.repositoryId, payload.repositoryId), + eq( + schema.githubSubscriptionsTable.repositoryId, + payload.repositoryId, + ), eq(schema.githubSubscriptionsTable.isEnabled, true), isNull(schema.githubSubscriptionsTable.deletedAt), ), @@ -96,19 +102,21 @@ export const GitHubWebhookWorkflowLayer = Cluster.GitHubWebhookWorkflow.toLayer( const ref = eventPayload.ref // Filter subscriptions by event type and branch filter - const eligibleSubscriptions = subscriptionsResult.subscriptions.filter((sub: Cluster.GitHubSubscriptionForWorkflow) => { - // Check if event type is enabled - if (!sub.enabledEvents.includes(internalEventType)) { - return false - } - - // For push events, check branch filter - if (payload.eventType === "push" && !GitHub.matchesBranchFilter(sub.branchFilter, ref)) { - return false - } - - return true - }) + const eligibleSubscriptions = subscriptionsResult.subscriptions.filter( + (sub: Cluster.GitHubSubscriptionForWorkflow) => { + // Check if event type is enabled + if (!sub.enabledEvents.includes(internalEventType)) { + return false + } + + // For push events, check branch filter + if (payload.eventType === "push" && !GitHub.matchesBranchFilter(sub.branchFilter, ref)) { + return false + } + + return true + }, + ) if (eligibleSubscriptions.length === 0) { yield* Effect.logDebug("No eligible subscriptions after filtering, workflow complete") diff --git a/apps/cluster/src/workflows/message-notification-handler.ts b/apps/cluster/src/workflows/message-notification-handler.ts index 66d675471..2370dec90 100644 --- a/apps/cluster/src/workflows/message-notification-handler.ts +++ b/apps/cluster/src/workflows/message-notification-handler.ts @@ -93,7 +93,9 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf if (shouldNotifyAll) { // DM/group or broadcast mention - notify all members (existing logic) - yield* Effect.logDebug(`Querying all channel members for channel ${payload.channelId}`) + yield* Effect.logDebug( + `Querying all channel members for channel ${payload.channelId}`, + ) const channelMembers = yield* db .execute((client) => @@ -129,7 +131,9 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf false, ), or( - isNull(schema.userPresenceStatusTable.activeChannelId), + isNull( + schema.userPresenceStatusTable.activeChannelId, + ), ne( schema.userPresenceStatusTable.activeChannelId, payload.channelId, @@ -234,7 +238,10 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf .from(schema.channelMembersTable) .leftJoin( schema.userPresenceStatusTable, - eq(schema.channelMembersTable.userId, schema.userPresenceStatusTable.userId), + eq( + schema.channelMembersTable.userId, + schema.userPresenceStatusTable.userId, + ), ) .where( and( @@ -247,7 +254,10 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf isNull(schema.userPresenceStatusTable.userId), // Has presence record - check suppressNotifications and activeChannel/status and( - eq(schema.userPresenceStatusTable.suppressNotifications, false), + eq( + schema.userPresenceStatusTable.suppressNotifications, + false, + ), or( isNull(schema.userPresenceStatusTable.activeChannelId), ne( @@ -308,10 +318,17 @@ export const MessageNotificationWorkflowLayer = Cluster.MessageNotificationWorkf execute: Effect.gen(function* () { const db = yield* Database.Database const startedAt = Date.now() - yield* Effect.annotateCurrentSpan("activity.candidate_count", membersResult.members.length) - yield* Effect.logDebug(`Creating notifications for ${membersResult.members.length} members`) + yield* Effect.annotateCurrentSpan( + "activity.candidate_count", + membersResult.members.length, + ) + yield* Effect.logDebug( + `Creating notifications for ${membersResult.members.length} members`, + ) - const userIds = membersResult.members.map((member: Cluster.ChannelMemberForNotification) => member.userId) + const userIds = membersResult.members.map( + (member: Cluster.ChannelMemberForNotification) => member.userId, + ) const orgMembers = yield* db .execute((client) => client diff --git a/apps/cluster/src/workflows/rss-feed-poll-handler.ts b/apps/cluster/src/workflows/rss-feed-poll-handler.ts index 01146e5a4..706c5541d 100644 --- a/apps/cluster/src/workflows/rss-feed-poll-handler.ts +++ b/apps/cluster/src/workflows/rss-feed-poll-handler.ts @@ -7,7 +7,10 @@ import { Effect, Option, Schema } from "effect" import { BotUserService } from "../services/bot-user-service.ts" export const RssFeedPollWorkflowLayer = Cluster.RssFeedPollWorkflow.toLayer( - Effect.fn("workflow.RssFeedPoll")(function* (payload: Cluster.RssFeedPollWorkflowPayload, _executionId: string) { + Effect.fn("workflow.RssFeedPoll")(function* ( + payload: Cluster.RssFeedPollWorkflowPayload, + _executionId: string, + ) { yield* Effect.annotateCurrentSpan("workflow.subscription_id", payload.subscriptionId) yield* Effect.annotateCurrentSpan("workflow.feed_url", payload.feedUrl) yield* Effect.annotateCurrentSpan("workflow.channel_id", payload.channelId) @@ -154,7 +157,8 @@ export const RssFeedPollWorkflowLayer = Cluster.RssFeedPollWorkflow.toLayer( const newItems = feedResult.items.filter((item) => { if (postedGuids.has(item.guid)) return false // Skip items published before the subscription was created - if (item.pubDate && new Date(item.pubDate).getTime() < payload.subscribedAt) return false + if (item.pubDate && new Date(item.pubDate).getTime() < payload.subscribedAt) + return false return true }) @@ -263,7 +267,9 @@ export const RssFeedPollWorkflowLayer = Cluster.RssFeedPollWorkflow.toLayer( .set({ lastFetchedAt: new Date(), lastItemGuid: lastItem?.guid ?? null, - lastItemPublishedAt: lastItem?.pubDate ? new Date(lastItem.pubDate) : null, + lastItemPublishedAt: lastItem?.pubDate + ? new Date(lastItem.pubDate) + : null, consecutiveErrors: 0, lastErrorMessage: null, lastErrorAt: null, diff --git a/apps/cluster/src/workflows/thread-naming-handler.ts b/apps/cluster/src/workflows/thread-naming-handler.ts index dc5bb8b8f..5a40f1807 100644 --- a/apps/cluster/src/workflows/thread-naming-handler.ts +++ b/apps/cluster/src/workflows/thread-naming-handler.ts @@ -20,7 +20,10 @@ Thread replies: Generate a concise thread name:` export const ThreadNamingWorkflowLayer = Cluster.ThreadNamingWorkflow.toLayer( - Effect.fn("workflow.ThreadNaming")(function* (payload: Cluster.ThreadNamingWorkflowPayload, _executionId: string) { + Effect.fn("workflow.ThreadNaming")(function* ( + payload: Cluster.ThreadNamingWorkflowPayload, + _executionId: string, + ) { yield* Effect.annotateCurrentSpan("workflow.thread_channel_id", payload.threadChannelId) yield* Effect.annotateCurrentSpan("workflow.original_message_id", payload.originalMessageId) @@ -210,30 +213,36 @@ export const ThreadNamingWorkflowLayer = Cluster.ThreadNamingWorkflow.toLayer( const response = yield* LanguageModel.generateText({ prompt, }).pipe( - Effect.catchTag("AiError", (err: AiError.AiError): Effect.Effect => { - const reason = err.reason - if (reason._tag === "RateLimitError") { + Effect.catchTag( + "AiError", + (err: AiError.AiError): Effect.Effect => { + const reason = err.reason + if (reason._tag === "RateLimitError") { + return Effect.fail( + new Cluster.AIRateLimitError({ + provider: "openrouter", + }), + ) + } + if ( + reason._tag === "InvalidOutputError" || + reason._tag === "StructuredOutputError" + ) { + return Effect.fail( + new Cluster.AIResponseParseError({ + threadChannelId: payload.threadChannelId, + rawResponse: reason.message, + }), + ) + } return Effect.fail( - new Cluster.AIRateLimitError({ + new Cluster.AIProviderUnavailableError({ provider: "openrouter", + cause: err, }), ) - } - if (reason._tag === "InvalidOutputError" || reason._tag === "StructuredOutputError") { - return Effect.fail( - new Cluster.AIResponseParseError({ - threadChannelId: payload.threadChannelId, - rawResponse: reason.message, - }), - ) - } - return Effect.fail( - new Cluster.AIProviderUnavailableError({ - provider: "openrouter", - cause: err, - }), - ) - }), + }, + ), ) // Clean up the response @@ -258,13 +267,14 @@ export const ThreadNamingWorkflowLayer = Cluster.ThreadNamingWorkflow.toLayer( }), }) }).pipe( - Effect.tapError((err: { readonly _tag: string; readonly provider?: string; readonly cause?: unknown }) => - Effect.logError("GenerateThreadName activity failed", { - threadChannelId: payload.threadChannelId, - errorTag: err._tag, - provider: err.provider, - cause: err.cause != null ? String(err.cause) : undefined, - }), + Effect.tapError( + (err: { readonly _tag: string; readonly provider?: string; readonly cause?: unknown }) => + Effect.logError("GenerateThreadName activity failed", { + threadChannelId: payload.threadChannelId, + errorTag: err._tag, + provider: err.provider, + cause: err.cause != null ? String(err.cause) : undefined, + }), ), ) @@ -299,7 +309,10 @@ export const ThreadNamingWorkflowLayer = Cluster.ThreadNamingWorkflow.toLayer( ), ) - yield* Effect.annotateCurrentSpan("activity.previous_name", contextResult.currentName ?? "") + yield* Effect.annotateCurrentSpan( + "activity.previous_name", + contextResult.currentName ?? "", + ) yield* Effect.annotateCurrentSpan("activity.new_name", nameResult.threadName) yield* Effect.logDebug( @@ -314,13 +327,14 @@ export const ThreadNamingWorkflowLayer = Cluster.ThreadNamingWorkflow.toLayer( }), }) }).pipe( - Effect.tapError((err: { readonly _tag: string; readonly newName?: string; readonly cause?: unknown }) => - Effect.logError("UpdateThreadName activity failed", { - threadChannelId: payload.threadChannelId, - errorTag: err._tag, - newName: err.newName, - cause: err.cause != null ? String(err.cause) : undefined, - }), + Effect.tapError( + (err: { readonly _tag: string; readonly newName?: string; readonly cause?: unknown }) => + Effect.logError("UpdateThreadName activity failed", { + threadChannelId: payload.threadChannelId, + errorTag: err._tag, + newName: err.newName, + cause: err.cause != null ? String(err.cause) : undefined, + }), ), ) diff --git a/apps/docs/src/routeTree.gen.ts b/apps/docs/src/routeTree.gen.ts index f759073a6..f9984f5d6 100644 --- a/apps/docs/src/routeTree.gen.ts +++ b/apps/docs/src/routeTree.gen.ts @@ -8,79 +8,77 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { Route as rootRouteImport } from './routes/__root' -import { Route as SplatRouteImport } from './routes/$' -import { Route as ApiSearchRouteImport } from './routes/api/search' +import { Route as rootRouteImport } from "./routes/__root" +import { Route as SplatRouteImport } from "./routes/$" +import { Route as ApiSearchRouteImport } from "./routes/api/search" const SplatRoute = SplatRouteImport.update({ - id: '/$', - path: '/$', - getParentRoute: () => rootRouteImport, + id: "/$", + path: "/$", + getParentRoute: () => rootRouteImport, } as any) const ApiSearchRoute = ApiSearchRouteImport.update({ - id: '/api/search', - path: '/api/search', - getParentRoute: () => rootRouteImport, + id: "/api/search", + path: "/api/search", + getParentRoute: () => rootRouteImport, } as any) export interface FileRoutesByFullPath { - '/$': typeof SplatRoute - '/api/search': typeof ApiSearchRoute + "/$": typeof SplatRoute + "/api/search": typeof ApiSearchRoute } export interface FileRoutesByTo { - '/$': typeof SplatRoute - '/api/search': typeof ApiSearchRoute + "/$": typeof SplatRoute + "/api/search": typeof ApiSearchRoute } export interface FileRoutesById { - __root__: typeof rootRouteImport - '/$': typeof SplatRoute - '/api/search': typeof ApiSearchRoute + __root__: typeof rootRouteImport + "/$": typeof SplatRoute + "/api/search": typeof ApiSearchRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/$' | '/api/search' - fileRoutesByTo: FileRoutesByTo - to: '/$' | '/api/search' - id: '__root__' | '/$' | '/api/search' - fileRoutesById: FileRoutesById + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: "/$" | "/api/search" + fileRoutesByTo: FileRoutesByTo + to: "/$" | "/api/search" + id: "__root__" | "/$" | "/api/search" + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - SplatRoute: typeof SplatRoute - ApiSearchRoute: typeof ApiSearchRoute + SplatRoute: typeof SplatRoute + ApiSearchRoute: typeof ApiSearchRoute } -declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/$': { - id: '/$' - path: '/$' - fullPath: '/$' - preLoaderRoute: typeof SplatRouteImport - parentRoute: typeof rootRouteImport - } - '/api/search': { - id: '/api/search' - path: '/api/search' - fullPath: '/api/search' - preLoaderRoute: typeof ApiSearchRouteImport - parentRoute: typeof rootRouteImport - } - } +declare module "@tanstack/react-router" { + interface FileRoutesByPath { + "/$": { + id: "/$" + path: "/$" + fullPath: "/$" + preLoaderRoute: typeof SplatRouteImport + parentRoute: typeof rootRouteImport + } + "/api/search": { + id: "/api/search" + path: "/api/search" + fullPath: "/api/search" + preLoaderRoute: typeof ApiSearchRouteImport + parentRoute: typeof rootRouteImport + } + } } const rootRouteChildren: RootRouteChildren = { - SplatRoute: SplatRoute, - ApiSearchRoute: ApiSearchRoute, + SplatRoute: SplatRoute, + ApiSearchRoute: ApiSearchRoute, } -export const routeTree = rootRouteImport - ._addFileChildren(rootRouteChildren) - ._addFileTypes() +export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes() -import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' -declare module '@tanstack/react-start' { - interface Register { - ssr: true - router: Awaited> - } +import type { getRouter } from "./router.tsx" +import type { createStart } from "@tanstack/react-start" +declare module "@tanstack/react-start" { + interface Register { + ssr: true + router: Awaited> + } } diff --git a/apps/electric-proxy/src/cache/access-context-service.ts b/apps/electric-proxy/src/cache/access-context-service.ts index 4e4f6515c..5473fd91b 100644 --- a/apps/electric-proxy/src/cache/access-context-service.ts +++ b/apps/electric-proxy/src/cache/access-context-service.ts @@ -72,17 +72,15 @@ export class AccessContextCacheService extends ServiceMap.Service - Effect.fail( - new AccessContextLookupError({ - message: "Failed to query bot's channels", - detail: error.message, - entityId: request.botId, - entityType: "bot", - }), - ), + Effect.catchTag("DatabaseError", (error) => + Effect.fail( + new AccessContextLookupError({ + message: "Failed to query bot's channels", + detail: error.message, + entityId: request.botId, + entityType: "bot", + }), + ), ), ) diff --git a/apps/electric-proxy/src/index.ts b/apps/electric-proxy/src/index.ts index decc585f8..15a337a69 100644 --- a/apps/electric-proxy/src/index.ts +++ b/apps/electric-proxy/src/index.ts @@ -138,7 +138,10 @@ const handleUserRequest = (request: Request) => { yield* annotateHandledError(401, "ProxyAuthenticationError") yield* Effect.logInfo("Authentication failed", { detail: error.detail }) yield* Metric.update( - Metric.withAttributes(proxyAuthFailures, { auth_type: "user", error_tag: "ProxyAuthenticationError" }), + Metric.withAttributes(proxyAuthFailures, { + auth_type: "user", + error_tag: "ProxyAuthenticationError", + }), 1, ) return new Response( @@ -320,7 +323,10 @@ const handleBotRequest = (request: Request) => { detail: error.detail, }) yield* Metric.update( - Metric.withAttributes(proxyAuthFailures, { auth_type: "bot", error_tag: "BotAuthenticationError" }), + Metric.withAttributes(proxyAuthFailures, { + auth_type: "bot", + error_tag: "BotAuthenticationError", + }), 1, ) return new Response( diff --git a/apps/link-preview-worker/src/handlers/link-preview.ts b/apps/link-preview-worker/src/handlers/link-preview.ts index 66f712c91..8f6e6f2c1 100644 --- a/apps/link-preview-worker/src/handlers/link-preview.ts +++ b/apps/link-preview-worker/src/handlers/link-preview.ts @@ -167,11 +167,7 @@ export const HttpLinkPreviewLive = HttpApiBuilder.group(LinkPreviewApi, "linkPre } // Store in cache (don't fail request if caching fails) - yield* cache - .set(cacheKey, result) - .pipe( - Effect.orElseSucceed(() => undefined), - ) + yield* cache.set(cacheKey, result).pipe(Effect.orElseSucceed(() => undefined)) return result }), diff --git a/apps/link-preview-worker/src/handlers/tweet.ts b/apps/link-preview-worker/src/handlers/tweet.ts index 0259955b1..c448d18bc 100644 --- a/apps/link-preview-worker/src/handlers/tweet.ts +++ b/apps/link-preview-worker/src/handlers/tweet.ts @@ -42,9 +42,7 @@ export const HttpTweetLive = HttpApiBuilder.group(LinkPreviewApi, "tweet", (hand yield* Effect.logDebug(`Successfully fetched tweet: ${tweetId}`) // Store in cache (don't fail request if caching fails) - yield* cache.set(cacheKey, tweet).pipe( - Effect.orElseSucceed(() => undefined), - ) + yield* cache.set(cacheKey, tweet).pipe(Effect.orElseSucceed(() => undefined)) // Return the tweet data return tweet diff --git a/apps/link-preview-worker/src/index.ts b/apps/link-preview-worker/src/index.ts index 6adacec00..ec6762447 100644 --- a/apps/link-preview-worker/src/index.ts +++ b/apps/link-preview-worker/src/index.ts @@ -7,16 +7,9 @@ import { HttpAppLive, HttpLinkPreviewLive, HttpTweetLive } from "./handle" import { TwitterApi } from "./services/twitter" const makeAppLayer = (env: Env) => { - const ServiceLayers = Layer.mergeAll( - makeKVCacheLayer(env.LINK_CACHE), - TwitterApi.layer, - ) + const ServiceLayers = Layer.mergeAll(makeKVCacheLayer(env.LINK_CACHE), TwitterApi.layer) - const HandlerLayers = Layer.mergeAll( - HttpAppLive, - HttpLinkPreviewLive, - HttpTweetLive, - ) + const HandlerLayers = Layer.mergeAll(HttpAppLive, HttpLinkPreviewLive, HttpTweetLive) return HttpApiBuilder.layer(LinkPreviewApi).pipe( Layer.provide(HandlerLayers), diff --git a/apps/web/src/components/chat/slate-editor/mention-element.tsx b/apps/web/src/components/chat/slate-editor/mention-element.tsx index cc1b85edf..4bc8f2533 100644 --- a/apps/web/src/components/chat/slate-editor/mention-element.tsx +++ b/apps/web/src/components/chat/slate-editor/mention-element.tsx @@ -44,7 +44,8 @@ export function MentionElement({ attributes, children, element, interactive = fa const userPresenceResult = useAtomValue( userWithPresenceAtomFamily((shouldFetchUser ? userId : "dummy-id") as UserId), ) - const data = shouldFetchUser && userPresenceResult ? AsyncResult.getOrElse(userPresenceResult, () => []) : [] + const data = + shouldFetchUser && userPresenceResult ? AsyncResult.getOrElse(userPresenceResult, () => []) : [] const result = data[0] const user = result?.user const presence = result?.presence diff --git a/apps/web/src/db/actions.ts b/apps/web/src/db/actions.ts index e9f2485fd..ad0f7fe3d 100644 --- a/apps/web/src/db/actions.ts +++ b/apps/web/src/db/actions.ts @@ -76,7 +76,7 @@ export const sendMessageAction = optimisticAction({ attachmentIds?: AttachmentId[] onRetryAttempt?: (attempt: number) => void }) => { - const messageId = props.messageId ?? crypto.randomUUID() as MessageId + const messageId = props.messageId ?? (crypto.randomUUID() as MessageId) messageCollection.insert({ id: messageId, @@ -415,7 +415,7 @@ export const createThreadAction = optimisticAction({ organizationId: OrganizationId currentUserId: UserId }) => { - const threadChannelId = props.threadChannelId ?? crypto.randomUUID() as ChannelId + const threadChannelId = props.threadChannelId ?? (crypto.randomUUID() as ChannelId) const now = new Date() // Create thread channel diff --git a/apps/web/src/lib/auth-token.ts b/apps/web/src/lib/auth-token.ts index f55cc5b0c..592941846 100644 --- a/apps/web/src/lib/auth-token.ts +++ b/apps/web/src/lib/auth-token.ts @@ -91,7 +91,9 @@ const readRefreshToken = Effect.fn("readRefreshToken")(function* () { } const storage = yield* WebTokenStorage return yield* storage.getRefreshToken -}).pipe(Effect.provide(isTauri() ? desktopStorageLive : webStorageLive)) as Effect.Effect> +}).pipe(Effect.provide(isTauri() ? desktopStorageLive : webStorageLive)) as Effect.Effect< + Option.Option +> /** Store tokens in the correct platform storage */ const storeTokens = (accessToken: string, refreshToken: string, expiresIn: number) => @@ -103,10 +105,7 @@ const storeTokens = (accessToken: string, refreshToken: string, expiresIn: numbe const storage = yield* WebTokenStorage yield* storage.storeTokens(accessToken, refreshToken, expiresIn) } - }).pipe( - Effect.provide(isTauri() ? desktopStorageLive : webStorageLive), - Effect.orDie, - ) + }).pipe(Effect.provide(isTauri() ? desktopStorageLive : webStorageLive), Effect.orDie) // ============================================================================ // Core Effects diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 97e3cf499..d4eb0d2d4 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -8,1420 +8,1362 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { Route as rootRouteImport } from './routes/__root' -import { Route as DevLayoutRouteImport } from './routes/_dev/layout' -import { Route as AppLayoutRouteImport } from './routes/_app/layout' -import { Route as AppIndexRouteImport } from './routes/_app/index' -import { Route as JoinSlugRouteImport } from './routes/join/$slug' -import { Route as AuthLoginRouteImport } from './routes/auth/login' -import { Route as AuthDesktopLoginRouteImport } from './routes/auth/desktop-login' -import { Route as AuthDesktopCallbackRouteImport } from './routes/auth/desktop-callback' -import { Route as AuthCallbackRouteImport } from './routes/auth/callback' -import { Route as DevUiLayoutRouteImport } from './routes/_dev/ui/layout' -import { Route as AppOrgSlugLayoutRouteImport } from './routes/_app/$orgSlug/layout' -import { Route as DevEmbedsIndexRouteImport } from './routes/dev/embeds/index' -import { Route as AppSelectOrganizationIndexRouteImport } from './routes/_app/select-organization/index' -import { Route as AppOnboardingIndexRouteImport } from './routes/_app/onboarding/index' -import { Route as AppOrgSlugIndexRouteImport } from './routes/_app/$orgSlug/index' -import { Route as DevEmbedsRailwayRouteImport } from './routes/dev/embeds/railway' -import { Route as DevEmbedsOpenstatusRouteImport } from './routes/dev/embeds/openstatus' -import { Route as DevEmbedsGithubRouteImport } from './routes/dev/embeds/github' -import { Route as DevEmbedsDemoRouteImport } from './routes/dev/embeds/demo' -import { Route as DevUiAgentStepsRouteImport } from './routes/_dev/ui/agent-steps' -import { Route as AppOnboardingSetupOrganizationRouteImport } from './routes/_app/onboarding/setup-organization' -import { Route as AppOrgSlugSettingsLayoutRouteImport } from './routes/_app/$orgSlug/settings/layout' -import { Route as AppOrgSlugNotificationsLayoutRouteImport } from './routes/_app/$orgSlug/notifications/layout' -import { Route as AppOrgSlugMySettingsLayoutRouteImport } from './routes/_app/$orgSlug/my-settings/layout' -import { Route as AppOrgSlugSettingsIndexRouteImport } from './routes/_app/$orgSlug/settings/index' -import { Route as AppOrgSlugNotificationsIndexRouteImport } from './routes/_app/$orgSlug/notifications/index' -import { Route as AppOrgSlugMySettingsIndexRouteImport } from './routes/_app/$orgSlug/my-settings/index' -import { Route as AppOrgSlugChatIndexRouteImport } from './routes/_app/$orgSlug/chat/index' -import { Route as AppOrgSlugSettingsTeamRouteImport } from './routes/_app/$orgSlug/settings/team' -import { Route as AppOrgSlugSettingsInvitationsRouteImport } from './routes/_app/$orgSlug/settings/invitations' -import { Route as AppOrgSlugSettingsDebugRouteImport } from './routes/_app/$orgSlug/settings/debug' -import { Route as AppOrgSlugSettingsCustomEmojisRouteImport } from './routes/_app/$orgSlug/settings/custom-emojis' -import { Route as AppOrgSlugSettingsConnectInvitesRouteImport } from './routes/_app/$orgSlug/settings/connect-invites' -import { Route as AppOrgSlugSettingsAuthenticationRouteImport } from './routes/_app/$orgSlug/settings/authentication' -import { Route as AppOrgSlugProfileUserIdRouteImport } from './routes/_app/$orgSlug/profile/$userId' -import { Route as AppOrgSlugNotificationsThreadsRouteImport } from './routes/_app/$orgSlug/notifications/threads' -import { Route as AppOrgSlugNotificationsGeneralRouteImport } from './routes/_app/$orgSlug/notifications/general' -import { Route as AppOrgSlugNotificationsDmsRouteImport } from './routes/_app/$orgSlug/notifications/dms' -import { Route as AppOrgSlugMySettingsProfileRouteImport } from './routes/_app/$orgSlug/my-settings/profile' -import { Route as AppOrgSlugMySettingsNotificationsRouteImport } from './routes/_app/$orgSlug/my-settings/notifications' -import { Route as AppOrgSlugMySettingsLinkedAccountsRouteImport } from './routes/_app/$orgSlug/my-settings/linked-accounts' -import { Route as AppOrgSlugMySettingsDesktopRouteImport } from './routes/_app/$orgSlug/my-settings/desktop' -import { Route as AppOrgSlugChatIdRouteImport } from './routes/_app/$orgSlug/chat/$id' -import { Route as AppOrgSlugSettingsIntegrationsLayoutRouteImport } from './routes/_app/$orgSlug/settings/integrations/layout' -import { Route as AppOrgSlugSettingsChatSyncLayoutRouteImport } from './routes/_app/$orgSlug/settings/chat-sync/layout' -import { Route as AppOrgSlugSettingsIntegrationsIndexRouteImport } from './routes/_app/$orgSlug/settings/integrations/index' -import { Route as AppOrgSlugSettingsChatSyncIndexRouteImport } from './routes/_app/$orgSlug/settings/chat-sync/index' -import { Route as AppOrgSlugChatIdIndexRouteImport } from './routes/_app/$orgSlug/chat/$id/index' -import { Route as AppOrgSlugSettingsIntegrationsYourAppsRouteImport } from './routes/_app/$orgSlug/settings/integrations/your-apps' -import { Route as AppOrgSlugSettingsIntegrationsMarketplaceRouteImport } from './routes/_app/$orgSlug/settings/integrations/marketplace' -import { Route as AppOrgSlugSettingsIntegrationsInstalledRouteImport } from './routes/_app/$orgSlug/settings/integrations/installed' -import { Route as AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport } from './routes/_app/$orgSlug/settings/integrations/$integrationId' -import { Route as AppOrgSlugSettingsChatSyncConnectionIdRouteImport } from './routes/_app/$orgSlug/settings/chat-sync/$connectionId' -import { Route as AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/layout' -import { Route as AppOrgSlugChatIdFilesIndexRouteImport } from './routes/_app/$orgSlug/chat/$id/files/index' -import { Route as AppOrgSlugChannelsChannelIdSettingsIndexRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/index' -import { Route as AppOrgSlugChatIdFilesMediaRouteImport } from './routes/_app/$orgSlug/chat/$id/files/media' -import { Route as AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/overview' -import { Route as AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/integrations' -import { Route as AppOrgSlugChannelsChannelIdSettingsConnectRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/connect' +import { Route as rootRouteImport } from "./routes/__root" +import { Route as DevLayoutRouteImport } from "./routes/_dev/layout" +import { Route as AppLayoutRouteImport } from "./routes/_app/layout" +import { Route as AppIndexRouteImport } from "./routes/_app/index" +import { Route as JoinSlugRouteImport } from "./routes/join/$slug" +import { Route as AuthLoginRouteImport } from "./routes/auth/login" +import { Route as AuthDesktopLoginRouteImport } from "./routes/auth/desktop-login" +import { Route as AuthDesktopCallbackRouteImport } from "./routes/auth/desktop-callback" +import { Route as AuthCallbackRouteImport } from "./routes/auth/callback" +import { Route as DevUiLayoutRouteImport } from "./routes/_dev/ui/layout" +import { Route as AppOrgSlugLayoutRouteImport } from "./routes/_app/$orgSlug/layout" +import { Route as DevEmbedsIndexRouteImport } from "./routes/dev/embeds/index" +import { Route as AppSelectOrganizationIndexRouteImport } from "./routes/_app/select-organization/index" +import { Route as AppOnboardingIndexRouteImport } from "./routes/_app/onboarding/index" +import { Route as AppOrgSlugIndexRouteImport } from "./routes/_app/$orgSlug/index" +import { Route as DevEmbedsRailwayRouteImport } from "./routes/dev/embeds/railway" +import { Route as DevEmbedsOpenstatusRouteImport } from "./routes/dev/embeds/openstatus" +import { Route as DevEmbedsGithubRouteImport } from "./routes/dev/embeds/github" +import { Route as DevEmbedsDemoRouteImport } from "./routes/dev/embeds/demo" +import { Route as DevUiAgentStepsRouteImport } from "./routes/_dev/ui/agent-steps" +import { Route as AppOnboardingSetupOrganizationRouteImport } from "./routes/_app/onboarding/setup-organization" +import { Route as AppOrgSlugSettingsLayoutRouteImport } from "./routes/_app/$orgSlug/settings/layout" +import { Route as AppOrgSlugNotificationsLayoutRouteImport } from "./routes/_app/$orgSlug/notifications/layout" +import { Route as AppOrgSlugMySettingsLayoutRouteImport } from "./routes/_app/$orgSlug/my-settings/layout" +import { Route as AppOrgSlugSettingsIndexRouteImport } from "./routes/_app/$orgSlug/settings/index" +import { Route as AppOrgSlugNotificationsIndexRouteImport } from "./routes/_app/$orgSlug/notifications/index" +import { Route as AppOrgSlugMySettingsIndexRouteImport } from "./routes/_app/$orgSlug/my-settings/index" +import { Route as AppOrgSlugChatIndexRouteImport } from "./routes/_app/$orgSlug/chat/index" +import { Route as AppOrgSlugSettingsTeamRouteImport } from "./routes/_app/$orgSlug/settings/team" +import { Route as AppOrgSlugSettingsInvitationsRouteImport } from "./routes/_app/$orgSlug/settings/invitations" +import { Route as AppOrgSlugSettingsDebugRouteImport } from "./routes/_app/$orgSlug/settings/debug" +import { Route as AppOrgSlugSettingsCustomEmojisRouteImport } from "./routes/_app/$orgSlug/settings/custom-emojis" +import { Route as AppOrgSlugSettingsConnectInvitesRouteImport } from "./routes/_app/$orgSlug/settings/connect-invites" +import { Route as AppOrgSlugSettingsAuthenticationRouteImport } from "./routes/_app/$orgSlug/settings/authentication" +import { Route as AppOrgSlugProfileUserIdRouteImport } from "./routes/_app/$orgSlug/profile/$userId" +import { Route as AppOrgSlugNotificationsThreadsRouteImport } from "./routes/_app/$orgSlug/notifications/threads" +import { Route as AppOrgSlugNotificationsGeneralRouteImport } from "./routes/_app/$orgSlug/notifications/general" +import { Route as AppOrgSlugNotificationsDmsRouteImport } from "./routes/_app/$orgSlug/notifications/dms" +import { Route as AppOrgSlugMySettingsProfileRouteImport } from "./routes/_app/$orgSlug/my-settings/profile" +import { Route as AppOrgSlugMySettingsNotificationsRouteImport } from "./routes/_app/$orgSlug/my-settings/notifications" +import { Route as AppOrgSlugMySettingsLinkedAccountsRouteImport } from "./routes/_app/$orgSlug/my-settings/linked-accounts" +import { Route as AppOrgSlugMySettingsDesktopRouteImport } from "./routes/_app/$orgSlug/my-settings/desktop" +import { Route as AppOrgSlugChatIdRouteImport } from "./routes/_app/$orgSlug/chat/$id" +import { Route as AppOrgSlugSettingsIntegrationsLayoutRouteImport } from "./routes/_app/$orgSlug/settings/integrations/layout" +import { Route as AppOrgSlugSettingsChatSyncLayoutRouteImport } from "./routes/_app/$orgSlug/settings/chat-sync/layout" +import { Route as AppOrgSlugSettingsIntegrationsIndexRouteImport } from "./routes/_app/$orgSlug/settings/integrations/index" +import { Route as AppOrgSlugSettingsChatSyncIndexRouteImport } from "./routes/_app/$orgSlug/settings/chat-sync/index" +import { Route as AppOrgSlugChatIdIndexRouteImport } from "./routes/_app/$orgSlug/chat/$id/index" +import { Route as AppOrgSlugSettingsIntegrationsYourAppsRouteImport } from "./routes/_app/$orgSlug/settings/integrations/your-apps" +import { Route as AppOrgSlugSettingsIntegrationsMarketplaceRouteImport } from "./routes/_app/$orgSlug/settings/integrations/marketplace" +import { Route as AppOrgSlugSettingsIntegrationsInstalledRouteImport } from "./routes/_app/$orgSlug/settings/integrations/installed" +import { Route as AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport } from "./routes/_app/$orgSlug/settings/integrations/$integrationId" +import { Route as AppOrgSlugSettingsChatSyncConnectionIdRouteImport } from "./routes/_app/$orgSlug/settings/chat-sync/$connectionId" +import { Route as AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/layout" +import { Route as AppOrgSlugChatIdFilesIndexRouteImport } from "./routes/_app/$orgSlug/chat/$id/files/index" +import { Route as AppOrgSlugChannelsChannelIdSettingsIndexRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/index" +import { Route as AppOrgSlugChatIdFilesMediaRouteImport } from "./routes/_app/$orgSlug/chat/$id/files/media" +import { Route as AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/overview" +import { Route as AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/integrations" +import { Route as AppOrgSlugChannelsChannelIdSettingsConnectRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/connect" const DevLayoutRoute = DevLayoutRouteImport.update({ - id: '/_dev', - getParentRoute: () => rootRouteImport, + id: "/_dev", + getParentRoute: () => rootRouteImport, } as any) const AppLayoutRoute = AppLayoutRouteImport.update({ - id: '/_app', - getParentRoute: () => rootRouteImport, + id: "/_app", + getParentRoute: () => rootRouteImport, } as any) const AppIndexRoute = AppIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppLayoutRoute, + id: "/", + path: "/", + getParentRoute: () => AppLayoutRoute, } as any) const JoinSlugRoute = JoinSlugRouteImport.update({ - id: '/join/$slug', - path: '/join/$slug', - getParentRoute: () => rootRouteImport, + id: "/join/$slug", + path: "/join/$slug", + getParentRoute: () => rootRouteImport, } as any) const AuthLoginRoute = AuthLoginRouteImport.update({ - id: '/auth/login', - path: '/auth/login', - getParentRoute: () => rootRouteImport, + id: "/auth/login", + path: "/auth/login", + getParentRoute: () => rootRouteImport, } as any) const AuthDesktopLoginRoute = AuthDesktopLoginRouteImport.update({ - id: '/auth/desktop-login', - path: '/auth/desktop-login', - getParentRoute: () => rootRouteImport, + id: "/auth/desktop-login", + path: "/auth/desktop-login", + getParentRoute: () => rootRouteImport, } as any) const AuthDesktopCallbackRoute = AuthDesktopCallbackRouteImport.update({ - id: '/auth/desktop-callback', - path: '/auth/desktop-callback', - getParentRoute: () => rootRouteImport, + id: "/auth/desktop-callback", + path: "/auth/desktop-callback", + getParentRoute: () => rootRouteImport, } as any) const AuthCallbackRoute = AuthCallbackRouteImport.update({ - id: '/auth/callback', - path: '/auth/callback', - getParentRoute: () => rootRouteImport, + id: "/auth/callback", + path: "/auth/callback", + getParentRoute: () => rootRouteImport, } as any) const DevUiLayoutRoute = DevUiLayoutRouteImport.update({ - id: '/ui', - path: '/ui', - getParentRoute: () => DevLayoutRoute, + id: "/ui", + path: "/ui", + getParentRoute: () => DevLayoutRoute, } as any) const AppOrgSlugLayoutRoute = AppOrgSlugLayoutRouteImport.update({ - id: '/$orgSlug', - path: '/$orgSlug', - getParentRoute: () => AppLayoutRoute, + id: "/$orgSlug", + path: "/$orgSlug", + getParentRoute: () => AppLayoutRoute, } as any) const DevEmbedsIndexRoute = DevEmbedsIndexRouteImport.update({ - id: '/dev/embeds/', - path: '/dev/embeds/', - getParentRoute: () => rootRouteImport, + id: "/dev/embeds/", + path: "/dev/embeds/", + getParentRoute: () => rootRouteImport, +} as any) +const AppSelectOrganizationIndexRoute = AppSelectOrganizationIndexRouteImport.update({ + id: "/select-organization/", + path: "/select-organization/", + getParentRoute: () => AppLayoutRoute, } as any) -const AppSelectOrganizationIndexRoute = - AppSelectOrganizationIndexRouteImport.update({ - id: '/select-organization/', - path: '/select-organization/', - getParentRoute: () => AppLayoutRoute, - } as any) const AppOnboardingIndexRoute = AppOnboardingIndexRouteImport.update({ - id: '/onboarding/', - path: '/onboarding/', - getParentRoute: () => AppLayoutRoute, + id: "/onboarding/", + path: "/onboarding/", + getParentRoute: () => AppLayoutRoute, } as any) const AppOrgSlugIndexRoute = AppOrgSlugIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugLayoutRoute, + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugLayoutRoute, } as any) const DevEmbedsRailwayRoute = DevEmbedsRailwayRouteImport.update({ - id: '/dev/embeds/railway', - path: '/dev/embeds/railway', - getParentRoute: () => rootRouteImport, + id: "/dev/embeds/railway", + path: "/dev/embeds/railway", + getParentRoute: () => rootRouteImport, } as any) const DevEmbedsOpenstatusRoute = DevEmbedsOpenstatusRouteImport.update({ - id: '/dev/embeds/openstatus', - path: '/dev/embeds/openstatus', - getParentRoute: () => rootRouteImport, + id: "/dev/embeds/openstatus", + path: "/dev/embeds/openstatus", + getParentRoute: () => rootRouteImport, } as any) const DevEmbedsGithubRoute = DevEmbedsGithubRouteImport.update({ - id: '/dev/embeds/github', - path: '/dev/embeds/github', - getParentRoute: () => rootRouteImport, + id: "/dev/embeds/github", + path: "/dev/embeds/github", + getParentRoute: () => rootRouteImport, } as any) const DevEmbedsDemoRoute = DevEmbedsDemoRouteImport.update({ - id: '/dev/embeds/demo', - path: '/dev/embeds/demo', - getParentRoute: () => rootRouteImport, + id: "/dev/embeds/demo", + path: "/dev/embeds/demo", + getParentRoute: () => rootRouteImport, } as any) const DevUiAgentStepsRoute = DevUiAgentStepsRouteImport.update({ - id: '/agent-steps', - path: '/agent-steps', - getParentRoute: () => DevUiLayoutRoute, + id: "/agent-steps", + path: "/agent-steps", + getParentRoute: () => DevUiLayoutRoute, +} as any) +const AppOnboardingSetupOrganizationRoute = AppOnboardingSetupOrganizationRouteImport.update({ + id: "/onboarding/setup-organization", + path: "/onboarding/setup-organization", + getParentRoute: () => AppLayoutRoute, +} as any) +const AppOrgSlugSettingsLayoutRoute = AppOrgSlugSettingsLayoutRouteImport.update({ + id: "/settings", + path: "/settings", + getParentRoute: () => AppOrgSlugLayoutRoute, +} as any) +const AppOrgSlugNotificationsLayoutRoute = AppOrgSlugNotificationsLayoutRouteImport.update({ + id: "/notifications", + path: "/notifications", + getParentRoute: () => AppOrgSlugLayoutRoute, +} as any) +const AppOrgSlugMySettingsLayoutRoute = AppOrgSlugMySettingsLayoutRouteImport.update({ + id: "/my-settings", + path: "/my-settings", + getParentRoute: () => AppOrgSlugLayoutRoute, } as any) -const AppOnboardingSetupOrganizationRoute = - AppOnboardingSetupOrganizationRouteImport.update({ - id: '/onboarding/setup-organization', - path: '/onboarding/setup-organization', - getParentRoute: () => AppLayoutRoute, - } as any) -const AppOrgSlugSettingsLayoutRoute = - AppOrgSlugSettingsLayoutRouteImport.update({ - id: '/settings', - path: '/settings', - getParentRoute: () => AppOrgSlugLayoutRoute, - } as any) -const AppOrgSlugNotificationsLayoutRoute = - AppOrgSlugNotificationsLayoutRouteImport.update({ - id: '/notifications', - path: '/notifications', - getParentRoute: () => AppOrgSlugLayoutRoute, - } as any) -const AppOrgSlugMySettingsLayoutRoute = - AppOrgSlugMySettingsLayoutRouteImport.update({ - id: '/my-settings', - path: '/my-settings', - getParentRoute: () => AppOrgSlugLayoutRoute, - } as any) const AppOrgSlugSettingsIndexRoute = AppOrgSlugSettingsIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugNotificationsIndexRoute = AppOrgSlugNotificationsIndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, +} as any) +const AppOrgSlugMySettingsIndexRoute = AppOrgSlugMySettingsIndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, } as any) -const AppOrgSlugNotificationsIndexRoute = - AppOrgSlugNotificationsIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, - } as any) -const AppOrgSlugMySettingsIndexRoute = - AppOrgSlugMySettingsIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, - } as any) const AppOrgSlugChatIndexRoute = AppOrgSlugChatIndexRouteImport.update({ - id: '/chat/', - path: '/chat/', - getParentRoute: () => AppOrgSlugLayoutRoute, + id: "/chat/", + path: "/chat/", + getParentRoute: () => AppOrgSlugLayoutRoute, } as any) const AppOrgSlugSettingsTeamRoute = AppOrgSlugSettingsTeamRouteImport.update({ - id: '/team', - path: '/team', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + id: "/team", + path: "/team", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugSettingsInvitationsRoute = AppOrgSlugSettingsInvitationsRouteImport.update({ + id: "/invitations", + path: "/invitations", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, } as any) -const AppOrgSlugSettingsInvitationsRoute = - AppOrgSlugSettingsInvitationsRouteImport.update({ - id: '/invitations', - path: '/invitations', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, - } as any) const AppOrgSlugSettingsDebugRoute = AppOrgSlugSettingsDebugRouteImport.update({ - id: '/debug', - path: '/debug', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + id: "/debug", + path: "/debug", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugSettingsCustomEmojisRoute = AppOrgSlugSettingsCustomEmojisRouteImport.update({ + id: "/custom-emojis", + path: "/custom-emojis", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugSettingsConnectInvitesRoute = AppOrgSlugSettingsConnectInvitesRouteImport.update({ + id: "/connect-invites", + path: "/connect-invites", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugSettingsAuthenticationRoute = AppOrgSlugSettingsAuthenticationRouteImport.update({ + id: "/authentication", + path: "/authentication", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, } as any) -const AppOrgSlugSettingsCustomEmojisRoute = - AppOrgSlugSettingsCustomEmojisRouteImport.update({ - id: '/custom-emojis', - path: '/custom-emojis', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, - } as any) -const AppOrgSlugSettingsConnectInvitesRoute = - AppOrgSlugSettingsConnectInvitesRouteImport.update({ - id: '/connect-invites', - path: '/connect-invites', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, - } as any) -const AppOrgSlugSettingsAuthenticationRoute = - AppOrgSlugSettingsAuthenticationRouteImport.update({ - id: '/authentication', - path: '/authentication', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, - } as any) const AppOrgSlugProfileUserIdRoute = AppOrgSlugProfileUserIdRouteImport.update({ - id: '/profile/$userId', - path: '/profile/$userId', - getParentRoute: () => AppOrgSlugLayoutRoute, + id: "/profile/$userId", + path: "/profile/$userId", + getParentRoute: () => AppOrgSlugLayoutRoute, +} as any) +const AppOrgSlugNotificationsThreadsRoute = AppOrgSlugNotificationsThreadsRouteImport.update({ + id: "/threads", + path: "/threads", + getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, +} as any) +const AppOrgSlugNotificationsGeneralRoute = AppOrgSlugNotificationsGeneralRouteImport.update({ + id: "/general", + path: "/general", + getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, +} as any) +const AppOrgSlugNotificationsDmsRoute = AppOrgSlugNotificationsDmsRouteImport.update({ + id: "/dms", + path: "/dms", + getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, +} as any) +const AppOrgSlugMySettingsProfileRoute = AppOrgSlugMySettingsProfileRouteImport.update({ + id: "/profile", + path: "/profile", + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, +} as any) +const AppOrgSlugMySettingsNotificationsRoute = AppOrgSlugMySettingsNotificationsRouteImport.update({ + id: "/notifications", + path: "/notifications", + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, +} as any) +const AppOrgSlugMySettingsLinkedAccountsRoute = AppOrgSlugMySettingsLinkedAccountsRouteImport.update({ + id: "/linked-accounts", + path: "/linked-accounts", + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, +} as any) +const AppOrgSlugMySettingsDesktopRoute = AppOrgSlugMySettingsDesktopRouteImport.update({ + id: "/desktop", + path: "/desktop", + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, } as any) -const AppOrgSlugNotificationsThreadsRoute = - AppOrgSlugNotificationsThreadsRouteImport.update({ - id: '/threads', - path: '/threads', - getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, - } as any) -const AppOrgSlugNotificationsGeneralRoute = - AppOrgSlugNotificationsGeneralRouteImport.update({ - id: '/general', - path: '/general', - getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, - } as any) -const AppOrgSlugNotificationsDmsRoute = - AppOrgSlugNotificationsDmsRouteImport.update({ - id: '/dms', - path: '/dms', - getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, - } as any) -const AppOrgSlugMySettingsProfileRoute = - AppOrgSlugMySettingsProfileRouteImport.update({ - id: '/profile', - path: '/profile', - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, - } as any) -const AppOrgSlugMySettingsNotificationsRoute = - AppOrgSlugMySettingsNotificationsRouteImport.update({ - id: '/notifications', - path: '/notifications', - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, - } as any) -const AppOrgSlugMySettingsLinkedAccountsRoute = - AppOrgSlugMySettingsLinkedAccountsRouteImport.update({ - id: '/linked-accounts', - path: '/linked-accounts', - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, - } as any) -const AppOrgSlugMySettingsDesktopRoute = - AppOrgSlugMySettingsDesktopRouteImport.update({ - id: '/desktop', - path: '/desktop', - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, - } as any) const AppOrgSlugChatIdRoute = AppOrgSlugChatIdRouteImport.update({ - id: '/chat/$id', - path: '/chat/$id', - getParentRoute: () => AppOrgSlugLayoutRoute, + id: "/chat/$id", + path: "/chat/$id", + getParentRoute: () => AppOrgSlugLayoutRoute, +} as any) +const AppOrgSlugSettingsIntegrationsLayoutRoute = AppOrgSlugSettingsIntegrationsLayoutRouteImport.update({ + id: "/integrations", + path: "/integrations", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugSettingsChatSyncLayoutRoute = AppOrgSlugSettingsChatSyncLayoutRouteImport.update({ + id: "/chat-sync", + path: "/chat-sync", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugSettingsIntegrationsIndexRoute = AppOrgSlugSettingsIntegrationsIndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, +} as any) +const AppOrgSlugSettingsChatSyncIndexRoute = AppOrgSlugSettingsChatSyncIndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugSettingsChatSyncLayoutRoute, } as any) -const AppOrgSlugSettingsIntegrationsLayoutRoute = - AppOrgSlugSettingsIntegrationsLayoutRouteImport.update({ - id: '/integrations', - path: '/integrations', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, - } as any) -const AppOrgSlugSettingsChatSyncLayoutRoute = - AppOrgSlugSettingsChatSyncLayoutRouteImport.update({ - id: '/chat-sync', - path: '/chat-sync', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, - } as any) -const AppOrgSlugSettingsIntegrationsIndexRoute = - AppOrgSlugSettingsIntegrationsIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) -const AppOrgSlugSettingsChatSyncIndexRoute = - AppOrgSlugSettingsChatSyncIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugSettingsChatSyncLayoutRoute, - } as any) const AppOrgSlugChatIdIndexRoute = AppOrgSlugChatIdIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugChatIdRoute, + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugChatIdRoute, +} as any) +const AppOrgSlugSettingsIntegrationsYourAppsRoute = AppOrgSlugSettingsIntegrationsYourAppsRouteImport.update({ + id: "/your-apps", + path: "/your-apps", + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, } as any) -const AppOrgSlugSettingsIntegrationsYourAppsRoute = - AppOrgSlugSettingsIntegrationsYourAppsRouteImport.update({ - id: '/your-apps', - path: '/your-apps', - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) const AppOrgSlugSettingsIntegrationsMarketplaceRoute = - AppOrgSlugSettingsIntegrationsMarketplaceRouteImport.update({ - id: '/marketplace', - path: '/marketplace', - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) + AppOrgSlugSettingsIntegrationsMarketplaceRouteImport.update({ + id: "/marketplace", + path: "/marketplace", + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, + } as any) const AppOrgSlugSettingsIntegrationsInstalledRoute = - AppOrgSlugSettingsIntegrationsInstalledRouteImport.update({ - id: '/installed', - path: '/installed', - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) + AppOrgSlugSettingsIntegrationsInstalledRouteImport.update({ + id: "/installed", + path: "/installed", + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, + } as any) const AppOrgSlugSettingsIntegrationsIntegrationIdRoute = - AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport.update({ - id: '/$integrationId', - path: '/$integrationId', - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) -const AppOrgSlugSettingsChatSyncConnectionIdRoute = - AppOrgSlugSettingsChatSyncConnectionIdRouteImport.update({ - id: '/$connectionId', - path: '/$connectionId', - getParentRoute: () => AppOrgSlugSettingsChatSyncLayoutRoute, - } as any) + AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport.update({ + id: "/$integrationId", + path: "/$integrationId", + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, + } as any) +const AppOrgSlugSettingsChatSyncConnectionIdRoute = AppOrgSlugSettingsChatSyncConnectionIdRouteImport.update({ + id: "/$connectionId", + path: "/$connectionId", + getParentRoute: () => AppOrgSlugSettingsChatSyncLayoutRoute, +} as any) const AppOrgSlugChannelsChannelIdSettingsLayoutRoute = - AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport.update({ - id: '/channels/$channelId/settings', - path: '/channels/$channelId/settings', - getParentRoute: () => AppOrgSlugLayoutRoute, - } as any) -const AppOrgSlugChatIdFilesIndexRoute = - AppOrgSlugChatIdFilesIndexRouteImport.update({ - id: '/files/', - path: '/files/', - getParentRoute: () => AppOrgSlugChatIdRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport.update({ + id: "/channels/$channelId/settings", + path: "/channels/$channelId/settings", + getParentRoute: () => AppOrgSlugLayoutRoute, + } as any) +const AppOrgSlugChatIdFilesIndexRoute = AppOrgSlugChatIdFilesIndexRouteImport.update({ + id: "/files/", + path: "/files/", + getParentRoute: () => AppOrgSlugChatIdRoute, +} as any) const AppOrgSlugChannelsChannelIdSettingsIndexRoute = - AppOrgSlugChannelsChannelIdSettingsIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, - } as any) -const AppOrgSlugChatIdFilesMediaRoute = - AppOrgSlugChatIdFilesMediaRouteImport.update({ - id: '/files/media', - path: '/files/media', - getParentRoute: () => AppOrgSlugChatIdRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsIndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, + } as any) +const AppOrgSlugChatIdFilesMediaRoute = AppOrgSlugChatIdFilesMediaRouteImport.update({ + id: "/files/media", + path: "/files/media", + getParentRoute: () => AppOrgSlugChatIdRoute, +} as any) const AppOrgSlugChannelsChannelIdSettingsOverviewRoute = - AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport.update({ - id: '/overview', - path: '/overview', - getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport.update({ + id: "/overview", + path: "/overview", + getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, + } as any) const AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute = - AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport.update({ - id: '/integrations', - path: '/integrations', - getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport.update({ + id: "/integrations", + path: "/integrations", + getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, + } as any) const AppOrgSlugChannelsChannelIdSettingsConnectRoute = - AppOrgSlugChannelsChannelIdSettingsConnectRouteImport.update({ - id: '/connect', - path: '/connect', - getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsConnectRouteImport.update({ + id: "/connect", + path: "/connect", + getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, + } as any) export interface FileRoutesByFullPath { - '/': typeof AppIndexRoute - '/$orgSlug': typeof AppOrgSlugLayoutRouteWithChildren - '/ui': typeof DevUiLayoutRouteWithChildren - '/auth/callback': typeof AuthCallbackRoute - '/auth/desktop-callback': typeof AuthDesktopCallbackRoute - '/auth/desktop-login': typeof AuthDesktopLoginRoute - '/auth/login': typeof AuthLoginRoute - '/join/$slug': typeof JoinSlugRoute - '/$orgSlug/my-settings': typeof AppOrgSlugMySettingsLayoutRouteWithChildren - '/$orgSlug/notifications': typeof AppOrgSlugNotificationsLayoutRouteWithChildren - '/$orgSlug/settings': typeof AppOrgSlugSettingsLayoutRouteWithChildren - '/onboarding/setup-organization': typeof AppOnboardingSetupOrganizationRoute - '/ui/agent-steps': typeof DevUiAgentStepsRoute - '/dev/embeds/demo': typeof DevEmbedsDemoRoute - '/dev/embeds/github': typeof DevEmbedsGithubRoute - '/dev/embeds/openstatus': typeof DevEmbedsOpenstatusRoute - '/dev/embeds/railway': typeof DevEmbedsRailwayRoute - '/$orgSlug/': typeof AppOrgSlugIndexRoute - '/onboarding/': typeof AppOnboardingIndexRoute - '/select-organization/': typeof AppSelectOrganizationIndexRoute - '/dev/embeds/': typeof DevEmbedsIndexRoute - '/$orgSlug/settings/chat-sync': typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren - '/$orgSlug/settings/integrations': typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren - '/$orgSlug/chat/$id': typeof AppOrgSlugChatIdRouteWithChildren - '/$orgSlug/my-settings/desktop': typeof AppOrgSlugMySettingsDesktopRoute - '/$orgSlug/my-settings/linked-accounts': typeof AppOrgSlugMySettingsLinkedAccountsRoute - '/$orgSlug/my-settings/notifications': typeof AppOrgSlugMySettingsNotificationsRoute - '/$orgSlug/my-settings/profile': typeof AppOrgSlugMySettingsProfileRoute - '/$orgSlug/notifications/dms': typeof AppOrgSlugNotificationsDmsRoute - '/$orgSlug/notifications/general': typeof AppOrgSlugNotificationsGeneralRoute - '/$orgSlug/notifications/threads': typeof AppOrgSlugNotificationsThreadsRoute - '/$orgSlug/profile/$userId': typeof AppOrgSlugProfileUserIdRoute - '/$orgSlug/settings/authentication': typeof AppOrgSlugSettingsAuthenticationRoute - '/$orgSlug/settings/connect-invites': typeof AppOrgSlugSettingsConnectInvitesRoute - '/$orgSlug/settings/custom-emojis': typeof AppOrgSlugSettingsCustomEmojisRoute - '/$orgSlug/settings/debug': typeof AppOrgSlugSettingsDebugRoute - '/$orgSlug/settings/invitations': typeof AppOrgSlugSettingsInvitationsRoute - '/$orgSlug/settings/team': typeof AppOrgSlugSettingsTeamRoute - '/$orgSlug/chat/': typeof AppOrgSlugChatIndexRoute - '/$orgSlug/my-settings/': typeof AppOrgSlugMySettingsIndexRoute - '/$orgSlug/notifications/': typeof AppOrgSlugNotificationsIndexRoute - '/$orgSlug/settings/': typeof AppOrgSlugSettingsIndexRoute - '/$orgSlug/channels/$channelId/settings': typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren - '/$orgSlug/settings/chat-sync/$connectionId': typeof AppOrgSlugSettingsChatSyncConnectionIdRoute - '/$orgSlug/settings/integrations/$integrationId': typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute - '/$orgSlug/settings/integrations/installed': typeof AppOrgSlugSettingsIntegrationsInstalledRoute - '/$orgSlug/settings/integrations/marketplace': typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute - '/$orgSlug/settings/integrations/your-apps': typeof AppOrgSlugSettingsIntegrationsYourAppsRoute - '/$orgSlug/chat/$id/': typeof AppOrgSlugChatIdIndexRoute - '/$orgSlug/settings/chat-sync/': typeof AppOrgSlugSettingsChatSyncIndexRoute - '/$orgSlug/settings/integrations/': typeof AppOrgSlugSettingsIntegrationsIndexRoute - '/$orgSlug/channels/$channelId/settings/connect': typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute - '/$orgSlug/channels/$channelId/settings/integrations': typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute - '/$orgSlug/channels/$channelId/settings/overview': typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute - '/$orgSlug/chat/$id/files/media': typeof AppOrgSlugChatIdFilesMediaRoute - '/$orgSlug/channels/$channelId/settings/': typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute - '/$orgSlug/chat/$id/files/': typeof AppOrgSlugChatIdFilesIndexRoute + "/": typeof AppIndexRoute + "/$orgSlug": typeof AppOrgSlugLayoutRouteWithChildren + "/ui": typeof DevUiLayoutRouteWithChildren + "/auth/callback": typeof AuthCallbackRoute + "/auth/desktop-callback": typeof AuthDesktopCallbackRoute + "/auth/desktop-login": typeof AuthDesktopLoginRoute + "/auth/login": typeof AuthLoginRoute + "/join/$slug": typeof JoinSlugRoute + "/$orgSlug/my-settings": typeof AppOrgSlugMySettingsLayoutRouteWithChildren + "/$orgSlug/notifications": typeof AppOrgSlugNotificationsLayoutRouteWithChildren + "/$orgSlug/settings": typeof AppOrgSlugSettingsLayoutRouteWithChildren + "/onboarding/setup-organization": typeof AppOnboardingSetupOrganizationRoute + "/ui/agent-steps": typeof DevUiAgentStepsRoute + "/dev/embeds/demo": typeof DevEmbedsDemoRoute + "/dev/embeds/github": typeof DevEmbedsGithubRoute + "/dev/embeds/openstatus": typeof DevEmbedsOpenstatusRoute + "/dev/embeds/railway": typeof DevEmbedsRailwayRoute + "/$orgSlug/": typeof AppOrgSlugIndexRoute + "/onboarding/": typeof AppOnboardingIndexRoute + "/select-organization/": typeof AppSelectOrganizationIndexRoute + "/dev/embeds/": typeof DevEmbedsIndexRoute + "/$orgSlug/settings/chat-sync": typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren + "/$orgSlug/settings/integrations": typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren + "/$orgSlug/chat/$id": typeof AppOrgSlugChatIdRouteWithChildren + "/$orgSlug/my-settings/desktop": typeof AppOrgSlugMySettingsDesktopRoute + "/$orgSlug/my-settings/linked-accounts": typeof AppOrgSlugMySettingsLinkedAccountsRoute + "/$orgSlug/my-settings/notifications": typeof AppOrgSlugMySettingsNotificationsRoute + "/$orgSlug/my-settings/profile": typeof AppOrgSlugMySettingsProfileRoute + "/$orgSlug/notifications/dms": typeof AppOrgSlugNotificationsDmsRoute + "/$orgSlug/notifications/general": typeof AppOrgSlugNotificationsGeneralRoute + "/$orgSlug/notifications/threads": typeof AppOrgSlugNotificationsThreadsRoute + "/$orgSlug/profile/$userId": typeof AppOrgSlugProfileUserIdRoute + "/$orgSlug/settings/authentication": typeof AppOrgSlugSettingsAuthenticationRoute + "/$orgSlug/settings/connect-invites": typeof AppOrgSlugSettingsConnectInvitesRoute + "/$orgSlug/settings/custom-emojis": typeof AppOrgSlugSettingsCustomEmojisRoute + "/$orgSlug/settings/debug": typeof AppOrgSlugSettingsDebugRoute + "/$orgSlug/settings/invitations": typeof AppOrgSlugSettingsInvitationsRoute + "/$orgSlug/settings/team": typeof AppOrgSlugSettingsTeamRoute + "/$orgSlug/chat/": typeof AppOrgSlugChatIndexRoute + "/$orgSlug/my-settings/": typeof AppOrgSlugMySettingsIndexRoute + "/$orgSlug/notifications/": typeof AppOrgSlugNotificationsIndexRoute + "/$orgSlug/settings/": typeof AppOrgSlugSettingsIndexRoute + "/$orgSlug/channels/$channelId/settings": typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren + "/$orgSlug/settings/chat-sync/$connectionId": typeof AppOrgSlugSettingsChatSyncConnectionIdRoute + "/$orgSlug/settings/integrations/$integrationId": typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute + "/$orgSlug/settings/integrations/installed": typeof AppOrgSlugSettingsIntegrationsInstalledRoute + "/$orgSlug/settings/integrations/marketplace": typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute + "/$orgSlug/settings/integrations/your-apps": typeof AppOrgSlugSettingsIntegrationsYourAppsRoute + "/$orgSlug/chat/$id/": typeof AppOrgSlugChatIdIndexRoute + "/$orgSlug/settings/chat-sync/": typeof AppOrgSlugSettingsChatSyncIndexRoute + "/$orgSlug/settings/integrations/": typeof AppOrgSlugSettingsIntegrationsIndexRoute + "/$orgSlug/channels/$channelId/settings/connect": typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute + "/$orgSlug/channels/$channelId/settings/integrations": typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute + "/$orgSlug/channels/$channelId/settings/overview": typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute + "/$orgSlug/chat/$id/files/media": typeof AppOrgSlugChatIdFilesMediaRoute + "/$orgSlug/channels/$channelId/settings/": typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute + "/$orgSlug/chat/$id/files/": typeof AppOrgSlugChatIdFilesIndexRoute } export interface FileRoutesByTo { - '/': typeof AppIndexRoute - '/ui': typeof DevUiLayoutRouteWithChildren - '/auth/callback': typeof AuthCallbackRoute - '/auth/desktop-callback': typeof AuthDesktopCallbackRoute - '/auth/desktop-login': typeof AuthDesktopLoginRoute - '/auth/login': typeof AuthLoginRoute - '/join/$slug': typeof JoinSlugRoute - '/onboarding/setup-organization': typeof AppOnboardingSetupOrganizationRoute - '/ui/agent-steps': typeof DevUiAgentStepsRoute - '/dev/embeds/demo': typeof DevEmbedsDemoRoute - '/dev/embeds/github': typeof DevEmbedsGithubRoute - '/dev/embeds/openstatus': typeof DevEmbedsOpenstatusRoute - '/dev/embeds/railway': typeof DevEmbedsRailwayRoute - '/$orgSlug': typeof AppOrgSlugIndexRoute - '/onboarding': typeof AppOnboardingIndexRoute - '/select-organization': typeof AppSelectOrganizationIndexRoute - '/dev/embeds': typeof DevEmbedsIndexRoute - '/$orgSlug/my-settings/desktop': typeof AppOrgSlugMySettingsDesktopRoute - '/$orgSlug/my-settings/linked-accounts': typeof AppOrgSlugMySettingsLinkedAccountsRoute - '/$orgSlug/my-settings/notifications': typeof AppOrgSlugMySettingsNotificationsRoute - '/$orgSlug/my-settings/profile': typeof AppOrgSlugMySettingsProfileRoute - '/$orgSlug/notifications/dms': typeof AppOrgSlugNotificationsDmsRoute - '/$orgSlug/notifications/general': typeof AppOrgSlugNotificationsGeneralRoute - '/$orgSlug/notifications/threads': typeof AppOrgSlugNotificationsThreadsRoute - '/$orgSlug/profile/$userId': typeof AppOrgSlugProfileUserIdRoute - '/$orgSlug/settings/authentication': typeof AppOrgSlugSettingsAuthenticationRoute - '/$orgSlug/settings/connect-invites': typeof AppOrgSlugSettingsConnectInvitesRoute - '/$orgSlug/settings/custom-emojis': typeof AppOrgSlugSettingsCustomEmojisRoute - '/$orgSlug/settings/debug': typeof AppOrgSlugSettingsDebugRoute - '/$orgSlug/settings/invitations': typeof AppOrgSlugSettingsInvitationsRoute - '/$orgSlug/settings/team': typeof AppOrgSlugSettingsTeamRoute - '/$orgSlug/chat': typeof AppOrgSlugChatIndexRoute - '/$orgSlug/my-settings': typeof AppOrgSlugMySettingsIndexRoute - '/$orgSlug/notifications': typeof AppOrgSlugNotificationsIndexRoute - '/$orgSlug/settings': typeof AppOrgSlugSettingsIndexRoute - '/$orgSlug/settings/chat-sync/$connectionId': typeof AppOrgSlugSettingsChatSyncConnectionIdRoute - '/$orgSlug/settings/integrations/$integrationId': typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute - '/$orgSlug/settings/integrations/installed': typeof AppOrgSlugSettingsIntegrationsInstalledRoute - '/$orgSlug/settings/integrations/marketplace': typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute - '/$orgSlug/settings/integrations/your-apps': typeof AppOrgSlugSettingsIntegrationsYourAppsRoute - '/$orgSlug/chat/$id': typeof AppOrgSlugChatIdIndexRoute - '/$orgSlug/settings/chat-sync': typeof AppOrgSlugSettingsChatSyncIndexRoute - '/$orgSlug/settings/integrations': typeof AppOrgSlugSettingsIntegrationsIndexRoute - '/$orgSlug/channels/$channelId/settings/connect': typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute - '/$orgSlug/channels/$channelId/settings/integrations': typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute - '/$orgSlug/channels/$channelId/settings/overview': typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute - '/$orgSlug/chat/$id/files/media': typeof AppOrgSlugChatIdFilesMediaRoute - '/$orgSlug/channels/$channelId/settings': typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute - '/$orgSlug/chat/$id/files': typeof AppOrgSlugChatIdFilesIndexRoute + "/": typeof AppIndexRoute + "/ui": typeof DevUiLayoutRouteWithChildren + "/auth/callback": typeof AuthCallbackRoute + "/auth/desktop-callback": typeof AuthDesktopCallbackRoute + "/auth/desktop-login": typeof AuthDesktopLoginRoute + "/auth/login": typeof AuthLoginRoute + "/join/$slug": typeof JoinSlugRoute + "/onboarding/setup-organization": typeof AppOnboardingSetupOrganizationRoute + "/ui/agent-steps": typeof DevUiAgentStepsRoute + "/dev/embeds/demo": typeof DevEmbedsDemoRoute + "/dev/embeds/github": typeof DevEmbedsGithubRoute + "/dev/embeds/openstatus": typeof DevEmbedsOpenstatusRoute + "/dev/embeds/railway": typeof DevEmbedsRailwayRoute + "/$orgSlug": typeof AppOrgSlugIndexRoute + "/onboarding": typeof AppOnboardingIndexRoute + "/select-organization": typeof AppSelectOrganizationIndexRoute + "/dev/embeds": typeof DevEmbedsIndexRoute + "/$orgSlug/my-settings/desktop": typeof AppOrgSlugMySettingsDesktopRoute + "/$orgSlug/my-settings/linked-accounts": typeof AppOrgSlugMySettingsLinkedAccountsRoute + "/$orgSlug/my-settings/notifications": typeof AppOrgSlugMySettingsNotificationsRoute + "/$orgSlug/my-settings/profile": typeof AppOrgSlugMySettingsProfileRoute + "/$orgSlug/notifications/dms": typeof AppOrgSlugNotificationsDmsRoute + "/$orgSlug/notifications/general": typeof AppOrgSlugNotificationsGeneralRoute + "/$orgSlug/notifications/threads": typeof AppOrgSlugNotificationsThreadsRoute + "/$orgSlug/profile/$userId": typeof AppOrgSlugProfileUserIdRoute + "/$orgSlug/settings/authentication": typeof AppOrgSlugSettingsAuthenticationRoute + "/$orgSlug/settings/connect-invites": typeof AppOrgSlugSettingsConnectInvitesRoute + "/$orgSlug/settings/custom-emojis": typeof AppOrgSlugSettingsCustomEmojisRoute + "/$orgSlug/settings/debug": typeof AppOrgSlugSettingsDebugRoute + "/$orgSlug/settings/invitations": typeof AppOrgSlugSettingsInvitationsRoute + "/$orgSlug/settings/team": typeof AppOrgSlugSettingsTeamRoute + "/$orgSlug/chat": typeof AppOrgSlugChatIndexRoute + "/$orgSlug/my-settings": typeof AppOrgSlugMySettingsIndexRoute + "/$orgSlug/notifications": typeof AppOrgSlugNotificationsIndexRoute + "/$orgSlug/settings": typeof AppOrgSlugSettingsIndexRoute + "/$orgSlug/settings/chat-sync/$connectionId": typeof AppOrgSlugSettingsChatSyncConnectionIdRoute + "/$orgSlug/settings/integrations/$integrationId": typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute + "/$orgSlug/settings/integrations/installed": typeof AppOrgSlugSettingsIntegrationsInstalledRoute + "/$orgSlug/settings/integrations/marketplace": typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute + "/$orgSlug/settings/integrations/your-apps": typeof AppOrgSlugSettingsIntegrationsYourAppsRoute + "/$orgSlug/chat/$id": typeof AppOrgSlugChatIdIndexRoute + "/$orgSlug/settings/chat-sync": typeof AppOrgSlugSettingsChatSyncIndexRoute + "/$orgSlug/settings/integrations": typeof AppOrgSlugSettingsIntegrationsIndexRoute + "/$orgSlug/channels/$channelId/settings/connect": typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute + "/$orgSlug/channels/$channelId/settings/integrations": typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute + "/$orgSlug/channels/$channelId/settings/overview": typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute + "/$orgSlug/chat/$id/files/media": typeof AppOrgSlugChatIdFilesMediaRoute + "/$orgSlug/channels/$channelId/settings": typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute + "/$orgSlug/chat/$id/files": typeof AppOrgSlugChatIdFilesIndexRoute } export interface FileRoutesById { - __root__: typeof rootRouteImport - '/_app': typeof AppLayoutRouteWithChildren - '/_dev': typeof DevLayoutRouteWithChildren - '/_app/$orgSlug': typeof AppOrgSlugLayoutRouteWithChildren - '/_dev/ui': typeof DevUiLayoutRouteWithChildren - '/auth/callback': typeof AuthCallbackRoute - '/auth/desktop-callback': typeof AuthDesktopCallbackRoute - '/auth/desktop-login': typeof AuthDesktopLoginRoute - '/auth/login': typeof AuthLoginRoute - '/join/$slug': typeof JoinSlugRoute - '/_app/': typeof AppIndexRoute - '/_app/$orgSlug/my-settings': typeof AppOrgSlugMySettingsLayoutRouteWithChildren - '/_app/$orgSlug/notifications': typeof AppOrgSlugNotificationsLayoutRouteWithChildren - '/_app/$orgSlug/settings': typeof AppOrgSlugSettingsLayoutRouteWithChildren - '/_app/onboarding/setup-organization': typeof AppOnboardingSetupOrganizationRoute - '/_dev/ui/agent-steps': typeof DevUiAgentStepsRoute - '/dev/embeds/demo': typeof DevEmbedsDemoRoute - '/dev/embeds/github': typeof DevEmbedsGithubRoute - '/dev/embeds/openstatus': typeof DevEmbedsOpenstatusRoute - '/dev/embeds/railway': typeof DevEmbedsRailwayRoute - '/_app/$orgSlug/': typeof AppOrgSlugIndexRoute - '/_app/onboarding/': typeof AppOnboardingIndexRoute - '/_app/select-organization/': typeof AppSelectOrganizationIndexRoute - '/dev/embeds/': typeof DevEmbedsIndexRoute - '/_app/$orgSlug/settings/chat-sync': typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren - '/_app/$orgSlug/settings/integrations': typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren - '/_app/$orgSlug/chat/$id': typeof AppOrgSlugChatIdRouteWithChildren - '/_app/$orgSlug/my-settings/desktop': typeof AppOrgSlugMySettingsDesktopRoute - '/_app/$orgSlug/my-settings/linked-accounts': typeof AppOrgSlugMySettingsLinkedAccountsRoute - '/_app/$orgSlug/my-settings/notifications': typeof AppOrgSlugMySettingsNotificationsRoute - '/_app/$orgSlug/my-settings/profile': typeof AppOrgSlugMySettingsProfileRoute - '/_app/$orgSlug/notifications/dms': typeof AppOrgSlugNotificationsDmsRoute - '/_app/$orgSlug/notifications/general': typeof AppOrgSlugNotificationsGeneralRoute - '/_app/$orgSlug/notifications/threads': typeof AppOrgSlugNotificationsThreadsRoute - '/_app/$orgSlug/profile/$userId': typeof AppOrgSlugProfileUserIdRoute - '/_app/$orgSlug/settings/authentication': typeof AppOrgSlugSettingsAuthenticationRoute - '/_app/$orgSlug/settings/connect-invites': typeof AppOrgSlugSettingsConnectInvitesRoute - '/_app/$orgSlug/settings/custom-emojis': typeof AppOrgSlugSettingsCustomEmojisRoute - '/_app/$orgSlug/settings/debug': typeof AppOrgSlugSettingsDebugRoute - '/_app/$orgSlug/settings/invitations': typeof AppOrgSlugSettingsInvitationsRoute - '/_app/$orgSlug/settings/team': typeof AppOrgSlugSettingsTeamRoute - '/_app/$orgSlug/chat/': typeof AppOrgSlugChatIndexRoute - '/_app/$orgSlug/my-settings/': typeof AppOrgSlugMySettingsIndexRoute - '/_app/$orgSlug/notifications/': typeof AppOrgSlugNotificationsIndexRoute - '/_app/$orgSlug/settings/': typeof AppOrgSlugSettingsIndexRoute - '/_app/$orgSlug/channels/$channelId/settings': typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren - '/_app/$orgSlug/settings/chat-sync/$connectionId': typeof AppOrgSlugSettingsChatSyncConnectionIdRoute - '/_app/$orgSlug/settings/integrations/$integrationId': typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute - '/_app/$orgSlug/settings/integrations/installed': typeof AppOrgSlugSettingsIntegrationsInstalledRoute - '/_app/$orgSlug/settings/integrations/marketplace': typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute - '/_app/$orgSlug/settings/integrations/your-apps': typeof AppOrgSlugSettingsIntegrationsYourAppsRoute - '/_app/$orgSlug/chat/$id/': typeof AppOrgSlugChatIdIndexRoute - '/_app/$orgSlug/settings/chat-sync/': typeof AppOrgSlugSettingsChatSyncIndexRoute - '/_app/$orgSlug/settings/integrations/': typeof AppOrgSlugSettingsIntegrationsIndexRoute - '/_app/$orgSlug/channels/$channelId/settings/connect': typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute - '/_app/$orgSlug/channels/$channelId/settings/integrations': typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute - '/_app/$orgSlug/channels/$channelId/settings/overview': typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute - '/_app/$orgSlug/chat/$id/files/media': typeof AppOrgSlugChatIdFilesMediaRoute - '/_app/$orgSlug/channels/$channelId/settings/': typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute - '/_app/$orgSlug/chat/$id/files/': typeof AppOrgSlugChatIdFilesIndexRoute + __root__: typeof rootRouteImport + "/_app": typeof AppLayoutRouteWithChildren + "/_dev": typeof DevLayoutRouteWithChildren + "/_app/$orgSlug": typeof AppOrgSlugLayoutRouteWithChildren + "/_dev/ui": typeof DevUiLayoutRouteWithChildren + "/auth/callback": typeof AuthCallbackRoute + "/auth/desktop-callback": typeof AuthDesktopCallbackRoute + "/auth/desktop-login": typeof AuthDesktopLoginRoute + "/auth/login": typeof AuthLoginRoute + "/join/$slug": typeof JoinSlugRoute + "/_app/": typeof AppIndexRoute + "/_app/$orgSlug/my-settings": typeof AppOrgSlugMySettingsLayoutRouteWithChildren + "/_app/$orgSlug/notifications": typeof AppOrgSlugNotificationsLayoutRouteWithChildren + "/_app/$orgSlug/settings": typeof AppOrgSlugSettingsLayoutRouteWithChildren + "/_app/onboarding/setup-organization": typeof AppOnboardingSetupOrganizationRoute + "/_dev/ui/agent-steps": typeof DevUiAgentStepsRoute + "/dev/embeds/demo": typeof DevEmbedsDemoRoute + "/dev/embeds/github": typeof DevEmbedsGithubRoute + "/dev/embeds/openstatus": typeof DevEmbedsOpenstatusRoute + "/dev/embeds/railway": typeof DevEmbedsRailwayRoute + "/_app/$orgSlug/": typeof AppOrgSlugIndexRoute + "/_app/onboarding/": typeof AppOnboardingIndexRoute + "/_app/select-organization/": typeof AppSelectOrganizationIndexRoute + "/dev/embeds/": typeof DevEmbedsIndexRoute + "/_app/$orgSlug/settings/chat-sync": typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren + "/_app/$orgSlug/settings/integrations": typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren + "/_app/$orgSlug/chat/$id": typeof AppOrgSlugChatIdRouteWithChildren + "/_app/$orgSlug/my-settings/desktop": typeof AppOrgSlugMySettingsDesktopRoute + "/_app/$orgSlug/my-settings/linked-accounts": typeof AppOrgSlugMySettingsLinkedAccountsRoute + "/_app/$orgSlug/my-settings/notifications": typeof AppOrgSlugMySettingsNotificationsRoute + "/_app/$orgSlug/my-settings/profile": typeof AppOrgSlugMySettingsProfileRoute + "/_app/$orgSlug/notifications/dms": typeof AppOrgSlugNotificationsDmsRoute + "/_app/$orgSlug/notifications/general": typeof AppOrgSlugNotificationsGeneralRoute + "/_app/$orgSlug/notifications/threads": typeof AppOrgSlugNotificationsThreadsRoute + "/_app/$orgSlug/profile/$userId": typeof AppOrgSlugProfileUserIdRoute + "/_app/$orgSlug/settings/authentication": typeof AppOrgSlugSettingsAuthenticationRoute + "/_app/$orgSlug/settings/connect-invites": typeof AppOrgSlugSettingsConnectInvitesRoute + "/_app/$orgSlug/settings/custom-emojis": typeof AppOrgSlugSettingsCustomEmojisRoute + "/_app/$orgSlug/settings/debug": typeof AppOrgSlugSettingsDebugRoute + "/_app/$orgSlug/settings/invitations": typeof AppOrgSlugSettingsInvitationsRoute + "/_app/$orgSlug/settings/team": typeof AppOrgSlugSettingsTeamRoute + "/_app/$orgSlug/chat/": typeof AppOrgSlugChatIndexRoute + "/_app/$orgSlug/my-settings/": typeof AppOrgSlugMySettingsIndexRoute + "/_app/$orgSlug/notifications/": typeof AppOrgSlugNotificationsIndexRoute + "/_app/$orgSlug/settings/": typeof AppOrgSlugSettingsIndexRoute + "/_app/$orgSlug/channels/$channelId/settings": typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren + "/_app/$orgSlug/settings/chat-sync/$connectionId": typeof AppOrgSlugSettingsChatSyncConnectionIdRoute + "/_app/$orgSlug/settings/integrations/$integrationId": typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute + "/_app/$orgSlug/settings/integrations/installed": typeof AppOrgSlugSettingsIntegrationsInstalledRoute + "/_app/$orgSlug/settings/integrations/marketplace": typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute + "/_app/$orgSlug/settings/integrations/your-apps": typeof AppOrgSlugSettingsIntegrationsYourAppsRoute + "/_app/$orgSlug/chat/$id/": typeof AppOrgSlugChatIdIndexRoute + "/_app/$orgSlug/settings/chat-sync/": typeof AppOrgSlugSettingsChatSyncIndexRoute + "/_app/$orgSlug/settings/integrations/": typeof AppOrgSlugSettingsIntegrationsIndexRoute + "/_app/$orgSlug/channels/$channelId/settings/connect": typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute + "/_app/$orgSlug/channels/$channelId/settings/integrations": typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute + "/_app/$orgSlug/channels/$channelId/settings/overview": typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute + "/_app/$orgSlug/chat/$id/files/media": typeof AppOrgSlugChatIdFilesMediaRoute + "/_app/$orgSlug/channels/$channelId/settings/": typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute + "/_app/$orgSlug/chat/$id/files/": typeof AppOrgSlugChatIdFilesIndexRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: - | '/' - | '/$orgSlug' - | '/ui' - | '/auth/callback' - | '/auth/desktop-callback' - | '/auth/desktop-login' - | '/auth/login' - | '/join/$slug' - | '/$orgSlug/my-settings' - | '/$orgSlug/notifications' - | '/$orgSlug/settings' - | '/onboarding/setup-organization' - | '/ui/agent-steps' - | '/dev/embeds/demo' - | '/dev/embeds/github' - | '/dev/embeds/openstatus' - | '/dev/embeds/railway' - | '/$orgSlug/' - | '/onboarding/' - | '/select-organization/' - | '/dev/embeds/' - | '/$orgSlug/settings/chat-sync' - | '/$orgSlug/settings/integrations' - | '/$orgSlug/chat/$id' - | '/$orgSlug/my-settings/desktop' - | '/$orgSlug/my-settings/linked-accounts' - | '/$orgSlug/my-settings/notifications' - | '/$orgSlug/my-settings/profile' - | '/$orgSlug/notifications/dms' - | '/$orgSlug/notifications/general' - | '/$orgSlug/notifications/threads' - | '/$orgSlug/profile/$userId' - | '/$orgSlug/settings/authentication' - | '/$orgSlug/settings/connect-invites' - | '/$orgSlug/settings/custom-emojis' - | '/$orgSlug/settings/debug' - | '/$orgSlug/settings/invitations' - | '/$orgSlug/settings/team' - | '/$orgSlug/chat/' - | '/$orgSlug/my-settings/' - | '/$orgSlug/notifications/' - | '/$orgSlug/settings/' - | '/$orgSlug/channels/$channelId/settings' - | '/$orgSlug/settings/chat-sync/$connectionId' - | '/$orgSlug/settings/integrations/$integrationId' - | '/$orgSlug/settings/integrations/installed' - | '/$orgSlug/settings/integrations/marketplace' - | '/$orgSlug/settings/integrations/your-apps' - | '/$orgSlug/chat/$id/' - | '/$orgSlug/settings/chat-sync/' - | '/$orgSlug/settings/integrations/' - | '/$orgSlug/channels/$channelId/settings/connect' - | '/$orgSlug/channels/$channelId/settings/integrations' - | '/$orgSlug/channels/$channelId/settings/overview' - | '/$orgSlug/chat/$id/files/media' - | '/$orgSlug/channels/$channelId/settings/' - | '/$orgSlug/chat/$id/files/' - fileRoutesByTo: FileRoutesByTo - to: - | '/' - | '/ui' - | '/auth/callback' - | '/auth/desktop-callback' - | '/auth/desktop-login' - | '/auth/login' - | '/join/$slug' - | '/onboarding/setup-organization' - | '/ui/agent-steps' - | '/dev/embeds/demo' - | '/dev/embeds/github' - | '/dev/embeds/openstatus' - | '/dev/embeds/railway' - | '/$orgSlug' - | '/onboarding' - | '/select-organization' - | '/dev/embeds' - | '/$orgSlug/my-settings/desktop' - | '/$orgSlug/my-settings/linked-accounts' - | '/$orgSlug/my-settings/notifications' - | '/$orgSlug/my-settings/profile' - | '/$orgSlug/notifications/dms' - | '/$orgSlug/notifications/general' - | '/$orgSlug/notifications/threads' - | '/$orgSlug/profile/$userId' - | '/$orgSlug/settings/authentication' - | '/$orgSlug/settings/connect-invites' - | '/$orgSlug/settings/custom-emojis' - | '/$orgSlug/settings/debug' - | '/$orgSlug/settings/invitations' - | '/$orgSlug/settings/team' - | '/$orgSlug/chat' - | '/$orgSlug/my-settings' - | '/$orgSlug/notifications' - | '/$orgSlug/settings' - | '/$orgSlug/settings/chat-sync/$connectionId' - | '/$orgSlug/settings/integrations/$integrationId' - | '/$orgSlug/settings/integrations/installed' - | '/$orgSlug/settings/integrations/marketplace' - | '/$orgSlug/settings/integrations/your-apps' - | '/$orgSlug/chat/$id' - | '/$orgSlug/settings/chat-sync' - | '/$orgSlug/settings/integrations' - | '/$orgSlug/channels/$channelId/settings/connect' - | '/$orgSlug/channels/$channelId/settings/integrations' - | '/$orgSlug/channels/$channelId/settings/overview' - | '/$orgSlug/chat/$id/files/media' - | '/$orgSlug/channels/$channelId/settings' - | '/$orgSlug/chat/$id/files' - id: - | '__root__' - | '/_app' - | '/_dev' - | '/_app/$orgSlug' - | '/_dev/ui' - | '/auth/callback' - | '/auth/desktop-callback' - | '/auth/desktop-login' - | '/auth/login' - | '/join/$slug' - | '/_app/' - | '/_app/$orgSlug/my-settings' - | '/_app/$orgSlug/notifications' - | '/_app/$orgSlug/settings' - | '/_app/onboarding/setup-organization' - | '/_dev/ui/agent-steps' - | '/dev/embeds/demo' - | '/dev/embeds/github' - | '/dev/embeds/openstatus' - | '/dev/embeds/railway' - | '/_app/$orgSlug/' - | '/_app/onboarding/' - | '/_app/select-organization/' - | '/dev/embeds/' - | '/_app/$orgSlug/settings/chat-sync' - | '/_app/$orgSlug/settings/integrations' - | '/_app/$orgSlug/chat/$id' - | '/_app/$orgSlug/my-settings/desktop' - | '/_app/$orgSlug/my-settings/linked-accounts' - | '/_app/$orgSlug/my-settings/notifications' - | '/_app/$orgSlug/my-settings/profile' - | '/_app/$orgSlug/notifications/dms' - | '/_app/$orgSlug/notifications/general' - | '/_app/$orgSlug/notifications/threads' - | '/_app/$orgSlug/profile/$userId' - | '/_app/$orgSlug/settings/authentication' - | '/_app/$orgSlug/settings/connect-invites' - | '/_app/$orgSlug/settings/custom-emojis' - | '/_app/$orgSlug/settings/debug' - | '/_app/$orgSlug/settings/invitations' - | '/_app/$orgSlug/settings/team' - | '/_app/$orgSlug/chat/' - | '/_app/$orgSlug/my-settings/' - | '/_app/$orgSlug/notifications/' - | '/_app/$orgSlug/settings/' - | '/_app/$orgSlug/channels/$channelId/settings' - | '/_app/$orgSlug/settings/chat-sync/$connectionId' - | '/_app/$orgSlug/settings/integrations/$integrationId' - | '/_app/$orgSlug/settings/integrations/installed' - | '/_app/$orgSlug/settings/integrations/marketplace' - | '/_app/$orgSlug/settings/integrations/your-apps' - | '/_app/$orgSlug/chat/$id/' - | '/_app/$orgSlug/settings/chat-sync/' - | '/_app/$orgSlug/settings/integrations/' - | '/_app/$orgSlug/channels/$channelId/settings/connect' - | '/_app/$orgSlug/channels/$channelId/settings/integrations' - | '/_app/$orgSlug/channels/$channelId/settings/overview' - | '/_app/$orgSlug/chat/$id/files/media' - | '/_app/$orgSlug/channels/$channelId/settings/' - | '/_app/$orgSlug/chat/$id/files/' - fileRoutesById: FileRoutesById + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | "/" + | "/$orgSlug" + | "/ui" + | "/auth/callback" + | "/auth/desktop-callback" + | "/auth/desktop-login" + | "/auth/login" + | "/join/$slug" + | "/$orgSlug/my-settings" + | "/$orgSlug/notifications" + | "/$orgSlug/settings" + | "/onboarding/setup-organization" + | "/ui/agent-steps" + | "/dev/embeds/demo" + | "/dev/embeds/github" + | "/dev/embeds/openstatus" + | "/dev/embeds/railway" + | "/$orgSlug/" + | "/onboarding/" + | "/select-organization/" + | "/dev/embeds/" + | "/$orgSlug/settings/chat-sync" + | "/$orgSlug/settings/integrations" + | "/$orgSlug/chat/$id" + | "/$orgSlug/my-settings/desktop" + | "/$orgSlug/my-settings/linked-accounts" + | "/$orgSlug/my-settings/notifications" + | "/$orgSlug/my-settings/profile" + | "/$orgSlug/notifications/dms" + | "/$orgSlug/notifications/general" + | "/$orgSlug/notifications/threads" + | "/$orgSlug/profile/$userId" + | "/$orgSlug/settings/authentication" + | "/$orgSlug/settings/connect-invites" + | "/$orgSlug/settings/custom-emojis" + | "/$orgSlug/settings/debug" + | "/$orgSlug/settings/invitations" + | "/$orgSlug/settings/team" + | "/$orgSlug/chat/" + | "/$orgSlug/my-settings/" + | "/$orgSlug/notifications/" + | "/$orgSlug/settings/" + | "/$orgSlug/channels/$channelId/settings" + | "/$orgSlug/settings/chat-sync/$connectionId" + | "/$orgSlug/settings/integrations/$integrationId" + | "/$orgSlug/settings/integrations/installed" + | "/$orgSlug/settings/integrations/marketplace" + | "/$orgSlug/settings/integrations/your-apps" + | "/$orgSlug/chat/$id/" + | "/$orgSlug/settings/chat-sync/" + | "/$orgSlug/settings/integrations/" + | "/$orgSlug/channels/$channelId/settings/connect" + | "/$orgSlug/channels/$channelId/settings/integrations" + | "/$orgSlug/channels/$channelId/settings/overview" + | "/$orgSlug/chat/$id/files/media" + | "/$orgSlug/channels/$channelId/settings/" + | "/$orgSlug/chat/$id/files/" + fileRoutesByTo: FileRoutesByTo + to: + | "/" + | "/ui" + | "/auth/callback" + | "/auth/desktop-callback" + | "/auth/desktop-login" + | "/auth/login" + | "/join/$slug" + | "/onboarding/setup-organization" + | "/ui/agent-steps" + | "/dev/embeds/demo" + | "/dev/embeds/github" + | "/dev/embeds/openstatus" + | "/dev/embeds/railway" + | "/$orgSlug" + | "/onboarding" + | "/select-organization" + | "/dev/embeds" + | "/$orgSlug/my-settings/desktop" + | "/$orgSlug/my-settings/linked-accounts" + | "/$orgSlug/my-settings/notifications" + | "/$orgSlug/my-settings/profile" + | "/$orgSlug/notifications/dms" + | "/$orgSlug/notifications/general" + | "/$orgSlug/notifications/threads" + | "/$orgSlug/profile/$userId" + | "/$orgSlug/settings/authentication" + | "/$orgSlug/settings/connect-invites" + | "/$orgSlug/settings/custom-emojis" + | "/$orgSlug/settings/debug" + | "/$orgSlug/settings/invitations" + | "/$orgSlug/settings/team" + | "/$orgSlug/chat" + | "/$orgSlug/my-settings" + | "/$orgSlug/notifications" + | "/$orgSlug/settings" + | "/$orgSlug/settings/chat-sync/$connectionId" + | "/$orgSlug/settings/integrations/$integrationId" + | "/$orgSlug/settings/integrations/installed" + | "/$orgSlug/settings/integrations/marketplace" + | "/$orgSlug/settings/integrations/your-apps" + | "/$orgSlug/chat/$id" + | "/$orgSlug/settings/chat-sync" + | "/$orgSlug/settings/integrations" + | "/$orgSlug/channels/$channelId/settings/connect" + | "/$orgSlug/channels/$channelId/settings/integrations" + | "/$orgSlug/channels/$channelId/settings/overview" + | "/$orgSlug/chat/$id/files/media" + | "/$orgSlug/channels/$channelId/settings" + | "/$orgSlug/chat/$id/files" + id: + | "__root__" + | "/_app" + | "/_dev" + | "/_app/$orgSlug" + | "/_dev/ui" + | "/auth/callback" + | "/auth/desktop-callback" + | "/auth/desktop-login" + | "/auth/login" + | "/join/$slug" + | "/_app/" + | "/_app/$orgSlug/my-settings" + | "/_app/$orgSlug/notifications" + | "/_app/$orgSlug/settings" + | "/_app/onboarding/setup-organization" + | "/_dev/ui/agent-steps" + | "/dev/embeds/demo" + | "/dev/embeds/github" + | "/dev/embeds/openstatus" + | "/dev/embeds/railway" + | "/_app/$orgSlug/" + | "/_app/onboarding/" + | "/_app/select-organization/" + | "/dev/embeds/" + | "/_app/$orgSlug/settings/chat-sync" + | "/_app/$orgSlug/settings/integrations" + | "/_app/$orgSlug/chat/$id" + | "/_app/$orgSlug/my-settings/desktop" + | "/_app/$orgSlug/my-settings/linked-accounts" + | "/_app/$orgSlug/my-settings/notifications" + | "/_app/$orgSlug/my-settings/profile" + | "/_app/$orgSlug/notifications/dms" + | "/_app/$orgSlug/notifications/general" + | "/_app/$orgSlug/notifications/threads" + | "/_app/$orgSlug/profile/$userId" + | "/_app/$orgSlug/settings/authentication" + | "/_app/$orgSlug/settings/connect-invites" + | "/_app/$orgSlug/settings/custom-emojis" + | "/_app/$orgSlug/settings/debug" + | "/_app/$orgSlug/settings/invitations" + | "/_app/$orgSlug/settings/team" + | "/_app/$orgSlug/chat/" + | "/_app/$orgSlug/my-settings/" + | "/_app/$orgSlug/notifications/" + | "/_app/$orgSlug/settings/" + | "/_app/$orgSlug/channels/$channelId/settings" + | "/_app/$orgSlug/settings/chat-sync/$connectionId" + | "/_app/$orgSlug/settings/integrations/$integrationId" + | "/_app/$orgSlug/settings/integrations/installed" + | "/_app/$orgSlug/settings/integrations/marketplace" + | "/_app/$orgSlug/settings/integrations/your-apps" + | "/_app/$orgSlug/chat/$id/" + | "/_app/$orgSlug/settings/chat-sync/" + | "/_app/$orgSlug/settings/integrations/" + | "/_app/$orgSlug/channels/$channelId/settings/connect" + | "/_app/$orgSlug/channels/$channelId/settings/integrations" + | "/_app/$orgSlug/channels/$channelId/settings/overview" + | "/_app/$orgSlug/chat/$id/files/media" + | "/_app/$orgSlug/channels/$channelId/settings/" + | "/_app/$orgSlug/chat/$id/files/" + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - AppLayoutRoute: typeof AppLayoutRouteWithChildren - DevLayoutRoute: typeof DevLayoutRouteWithChildren - AuthCallbackRoute: typeof AuthCallbackRoute - AuthDesktopCallbackRoute: typeof AuthDesktopCallbackRoute - AuthDesktopLoginRoute: typeof AuthDesktopLoginRoute - AuthLoginRoute: typeof AuthLoginRoute - JoinSlugRoute: typeof JoinSlugRoute - DevEmbedsDemoRoute: typeof DevEmbedsDemoRoute - DevEmbedsGithubRoute: typeof DevEmbedsGithubRoute - DevEmbedsOpenstatusRoute: typeof DevEmbedsOpenstatusRoute - DevEmbedsRailwayRoute: typeof DevEmbedsRailwayRoute - DevEmbedsIndexRoute: typeof DevEmbedsIndexRoute + AppLayoutRoute: typeof AppLayoutRouteWithChildren + DevLayoutRoute: typeof DevLayoutRouteWithChildren + AuthCallbackRoute: typeof AuthCallbackRoute + AuthDesktopCallbackRoute: typeof AuthDesktopCallbackRoute + AuthDesktopLoginRoute: typeof AuthDesktopLoginRoute + AuthLoginRoute: typeof AuthLoginRoute + JoinSlugRoute: typeof JoinSlugRoute + DevEmbedsDemoRoute: typeof DevEmbedsDemoRoute + DevEmbedsGithubRoute: typeof DevEmbedsGithubRoute + DevEmbedsOpenstatusRoute: typeof DevEmbedsOpenstatusRoute + DevEmbedsRailwayRoute: typeof DevEmbedsRailwayRoute + DevEmbedsIndexRoute: typeof DevEmbedsIndexRoute } -declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/_dev': { - id: '/_dev' - path: '' - fullPath: '/' - preLoaderRoute: typeof DevLayoutRouteImport - parentRoute: typeof rootRouteImport - } - '/_app': { - id: '/_app' - path: '' - fullPath: '/' - preLoaderRoute: typeof AppLayoutRouteImport - parentRoute: typeof rootRouteImport - } - '/_app/': { - id: '/_app/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof AppIndexRouteImport - parentRoute: typeof AppLayoutRoute - } - '/join/$slug': { - id: '/join/$slug' - path: '/join/$slug' - fullPath: '/join/$slug' - preLoaderRoute: typeof JoinSlugRouteImport - parentRoute: typeof rootRouteImport - } - '/auth/login': { - id: '/auth/login' - path: '/auth/login' - fullPath: '/auth/login' - preLoaderRoute: typeof AuthLoginRouteImport - parentRoute: typeof rootRouteImport - } - '/auth/desktop-login': { - id: '/auth/desktop-login' - path: '/auth/desktop-login' - fullPath: '/auth/desktop-login' - preLoaderRoute: typeof AuthDesktopLoginRouteImport - parentRoute: typeof rootRouteImport - } - '/auth/desktop-callback': { - id: '/auth/desktop-callback' - path: '/auth/desktop-callback' - fullPath: '/auth/desktop-callback' - preLoaderRoute: typeof AuthDesktopCallbackRouteImport - parentRoute: typeof rootRouteImport - } - '/auth/callback': { - id: '/auth/callback' - path: '/auth/callback' - fullPath: '/auth/callback' - preLoaderRoute: typeof AuthCallbackRouteImport - parentRoute: typeof rootRouteImport - } - '/_dev/ui': { - id: '/_dev/ui' - path: '/ui' - fullPath: '/ui' - preLoaderRoute: typeof DevUiLayoutRouteImport - parentRoute: typeof DevLayoutRoute - } - '/_app/$orgSlug': { - id: '/_app/$orgSlug' - path: '/$orgSlug' - fullPath: '/$orgSlug' - preLoaderRoute: typeof AppOrgSlugLayoutRouteImport - parentRoute: typeof AppLayoutRoute - } - '/dev/embeds/': { - id: '/dev/embeds/' - path: '/dev/embeds' - fullPath: '/dev/embeds/' - preLoaderRoute: typeof DevEmbedsIndexRouteImport - parentRoute: typeof rootRouteImport - } - '/_app/select-organization/': { - id: '/_app/select-organization/' - path: '/select-organization' - fullPath: '/select-organization/' - preLoaderRoute: typeof AppSelectOrganizationIndexRouteImport - parentRoute: typeof AppLayoutRoute - } - '/_app/onboarding/': { - id: '/_app/onboarding/' - path: '/onboarding' - fullPath: '/onboarding/' - preLoaderRoute: typeof AppOnboardingIndexRouteImport - parentRoute: typeof AppLayoutRoute - } - '/_app/$orgSlug/': { - id: '/_app/$orgSlug/' - path: '/' - fullPath: '/$orgSlug/' - preLoaderRoute: typeof AppOrgSlugIndexRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/dev/embeds/railway': { - id: '/dev/embeds/railway' - path: '/dev/embeds/railway' - fullPath: '/dev/embeds/railway' - preLoaderRoute: typeof DevEmbedsRailwayRouteImport - parentRoute: typeof rootRouteImport - } - '/dev/embeds/openstatus': { - id: '/dev/embeds/openstatus' - path: '/dev/embeds/openstatus' - fullPath: '/dev/embeds/openstatus' - preLoaderRoute: typeof DevEmbedsOpenstatusRouteImport - parentRoute: typeof rootRouteImport - } - '/dev/embeds/github': { - id: '/dev/embeds/github' - path: '/dev/embeds/github' - fullPath: '/dev/embeds/github' - preLoaderRoute: typeof DevEmbedsGithubRouteImport - parentRoute: typeof rootRouteImport - } - '/dev/embeds/demo': { - id: '/dev/embeds/demo' - path: '/dev/embeds/demo' - fullPath: '/dev/embeds/demo' - preLoaderRoute: typeof DevEmbedsDemoRouteImport - parentRoute: typeof rootRouteImport - } - '/_dev/ui/agent-steps': { - id: '/_dev/ui/agent-steps' - path: '/agent-steps' - fullPath: '/ui/agent-steps' - preLoaderRoute: typeof DevUiAgentStepsRouteImport - parentRoute: typeof DevUiLayoutRoute - } - '/_app/onboarding/setup-organization': { - id: '/_app/onboarding/setup-organization' - path: '/onboarding/setup-organization' - fullPath: '/onboarding/setup-organization' - preLoaderRoute: typeof AppOnboardingSetupOrganizationRouteImport - parentRoute: typeof AppLayoutRoute - } - '/_app/$orgSlug/settings': { - id: '/_app/$orgSlug/settings' - path: '/settings' - fullPath: '/$orgSlug/settings' - preLoaderRoute: typeof AppOrgSlugSettingsLayoutRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/notifications': { - id: '/_app/$orgSlug/notifications' - path: '/notifications' - fullPath: '/$orgSlug/notifications' - preLoaderRoute: typeof AppOrgSlugNotificationsLayoutRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/my-settings': { - id: '/_app/$orgSlug/my-settings' - path: '/my-settings' - fullPath: '/$orgSlug/my-settings' - preLoaderRoute: typeof AppOrgSlugMySettingsLayoutRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/settings/': { - id: '/_app/$orgSlug/settings/' - path: '/' - fullPath: '/$orgSlug/settings/' - preLoaderRoute: typeof AppOrgSlugSettingsIndexRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/notifications/': { - id: '/_app/$orgSlug/notifications/' - path: '/' - fullPath: '/$orgSlug/notifications/' - preLoaderRoute: typeof AppOrgSlugNotificationsIndexRouteImport - parentRoute: typeof AppOrgSlugNotificationsLayoutRoute - } - '/_app/$orgSlug/my-settings/': { - id: '/_app/$orgSlug/my-settings/' - path: '/' - fullPath: '/$orgSlug/my-settings/' - preLoaderRoute: typeof AppOrgSlugMySettingsIndexRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - '/_app/$orgSlug/chat/': { - id: '/_app/$orgSlug/chat/' - path: '/chat' - fullPath: '/$orgSlug/chat/' - preLoaderRoute: typeof AppOrgSlugChatIndexRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/settings/team': { - id: '/_app/$orgSlug/settings/team' - path: '/team' - fullPath: '/$orgSlug/settings/team' - preLoaderRoute: typeof AppOrgSlugSettingsTeamRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/invitations': { - id: '/_app/$orgSlug/settings/invitations' - path: '/invitations' - fullPath: '/$orgSlug/settings/invitations' - preLoaderRoute: typeof AppOrgSlugSettingsInvitationsRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/debug': { - id: '/_app/$orgSlug/settings/debug' - path: '/debug' - fullPath: '/$orgSlug/settings/debug' - preLoaderRoute: typeof AppOrgSlugSettingsDebugRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/custom-emojis': { - id: '/_app/$orgSlug/settings/custom-emojis' - path: '/custom-emojis' - fullPath: '/$orgSlug/settings/custom-emojis' - preLoaderRoute: typeof AppOrgSlugSettingsCustomEmojisRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/connect-invites': { - id: '/_app/$orgSlug/settings/connect-invites' - path: '/connect-invites' - fullPath: '/$orgSlug/settings/connect-invites' - preLoaderRoute: typeof AppOrgSlugSettingsConnectInvitesRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/authentication': { - id: '/_app/$orgSlug/settings/authentication' - path: '/authentication' - fullPath: '/$orgSlug/settings/authentication' - preLoaderRoute: typeof AppOrgSlugSettingsAuthenticationRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/profile/$userId': { - id: '/_app/$orgSlug/profile/$userId' - path: '/profile/$userId' - fullPath: '/$orgSlug/profile/$userId' - preLoaderRoute: typeof AppOrgSlugProfileUserIdRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/notifications/threads': { - id: '/_app/$orgSlug/notifications/threads' - path: '/threads' - fullPath: '/$orgSlug/notifications/threads' - preLoaderRoute: typeof AppOrgSlugNotificationsThreadsRouteImport - parentRoute: typeof AppOrgSlugNotificationsLayoutRoute - } - '/_app/$orgSlug/notifications/general': { - id: '/_app/$orgSlug/notifications/general' - path: '/general' - fullPath: '/$orgSlug/notifications/general' - preLoaderRoute: typeof AppOrgSlugNotificationsGeneralRouteImport - parentRoute: typeof AppOrgSlugNotificationsLayoutRoute - } - '/_app/$orgSlug/notifications/dms': { - id: '/_app/$orgSlug/notifications/dms' - path: '/dms' - fullPath: '/$orgSlug/notifications/dms' - preLoaderRoute: typeof AppOrgSlugNotificationsDmsRouteImport - parentRoute: typeof AppOrgSlugNotificationsLayoutRoute - } - '/_app/$orgSlug/my-settings/profile': { - id: '/_app/$orgSlug/my-settings/profile' - path: '/profile' - fullPath: '/$orgSlug/my-settings/profile' - preLoaderRoute: typeof AppOrgSlugMySettingsProfileRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - '/_app/$orgSlug/my-settings/notifications': { - id: '/_app/$orgSlug/my-settings/notifications' - path: '/notifications' - fullPath: '/$orgSlug/my-settings/notifications' - preLoaderRoute: typeof AppOrgSlugMySettingsNotificationsRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - '/_app/$orgSlug/my-settings/linked-accounts': { - id: '/_app/$orgSlug/my-settings/linked-accounts' - path: '/linked-accounts' - fullPath: '/$orgSlug/my-settings/linked-accounts' - preLoaderRoute: typeof AppOrgSlugMySettingsLinkedAccountsRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - '/_app/$orgSlug/my-settings/desktop': { - id: '/_app/$orgSlug/my-settings/desktop' - path: '/desktop' - fullPath: '/$orgSlug/my-settings/desktop' - preLoaderRoute: typeof AppOrgSlugMySettingsDesktopRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - '/_app/$orgSlug/chat/$id': { - id: '/_app/$orgSlug/chat/$id' - path: '/chat/$id' - fullPath: '/$orgSlug/chat/$id' - preLoaderRoute: typeof AppOrgSlugChatIdRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/settings/integrations': { - id: '/_app/$orgSlug/settings/integrations' - path: '/integrations' - fullPath: '/$orgSlug/settings/integrations' - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/chat-sync': { - id: '/_app/$orgSlug/settings/chat-sync' - path: '/chat-sync' - fullPath: '/$orgSlug/settings/chat-sync' - preLoaderRoute: typeof AppOrgSlugSettingsChatSyncLayoutRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/integrations/': { - id: '/_app/$orgSlug/settings/integrations/' - path: '/' - fullPath: '/$orgSlug/settings/integrations/' - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsIndexRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - '/_app/$orgSlug/settings/chat-sync/': { - id: '/_app/$orgSlug/settings/chat-sync/' - path: '/' - fullPath: '/$orgSlug/settings/chat-sync/' - preLoaderRoute: typeof AppOrgSlugSettingsChatSyncIndexRouteImport - parentRoute: typeof AppOrgSlugSettingsChatSyncLayoutRoute - } - '/_app/$orgSlug/chat/$id/': { - id: '/_app/$orgSlug/chat/$id/' - path: '/' - fullPath: '/$orgSlug/chat/$id/' - preLoaderRoute: typeof AppOrgSlugChatIdIndexRouteImport - parentRoute: typeof AppOrgSlugChatIdRoute - } - '/_app/$orgSlug/settings/integrations/your-apps': { - id: '/_app/$orgSlug/settings/integrations/your-apps' - path: '/your-apps' - fullPath: '/$orgSlug/settings/integrations/your-apps' - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsYourAppsRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - '/_app/$orgSlug/settings/integrations/marketplace': { - id: '/_app/$orgSlug/settings/integrations/marketplace' - path: '/marketplace' - fullPath: '/$orgSlug/settings/integrations/marketplace' - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsMarketplaceRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - '/_app/$orgSlug/settings/integrations/installed': { - id: '/_app/$orgSlug/settings/integrations/installed' - path: '/installed' - fullPath: '/$orgSlug/settings/integrations/installed' - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsInstalledRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - '/_app/$orgSlug/settings/integrations/$integrationId': { - id: '/_app/$orgSlug/settings/integrations/$integrationId' - path: '/$integrationId' - fullPath: '/$orgSlug/settings/integrations/$integrationId' - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - '/_app/$orgSlug/settings/chat-sync/$connectionId': { - id: '/_app/$orgSlug/settings/chat-sync/$connectionId' - path: '/$connectionId' - fullPath: '/$orgSlug/settings/chat-sync/$connectionId' - preLoaderRoute: typeof AppOrgSlugSettingsChatSyncConnectionIdRouteImport - parentRoute: typeof AppOrgSlugSettingsChatSyncLayoutRoute - } - '/_app/$orgSlug/channels/$channelId/settings': { - id: '/_app/$orgSlug/channels/$channelId/settings' - path: '/channels/$channelId/settings' - fullPath: '/$orgSlug/channels/$channelId/settings' - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/chat/$id/files/': { - id: '/_app/$orgSlug/chat/$id/files/' - path: '/files' - fullPath: '/$orgSlug/chat/$id/files/' - preLoaderRoute: typeof AppOrgSlugChatIdFilesIndexRouteImport - parentRoute: typeof AppOrgSlugChatIdRoute - } - '/_app/$orgSlug/channels/$channelId/settings/': { - id: '/_app/$orgSlug/channels/$channelId/settings/' - path: '/' - fullPath: '/$orgSlug/channels/$channelId/settings/' - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsIndexRouteImport - parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute - } - '/_app/$orgSlug/chat/$id/files/media': { - id: '/_app/$orgSlug/chat/$id/files/media' - path: '/files/media' - fullPath: '/$orgSlug/chat/$id/files/media' - preLoaderRoute: typeof AppOrgSlugChatIdFilesMediaRouteImport - parentRoute: typeof AppOrgSlugChatIdRoute - } - '/_app/$orgSlug/channels/$channelId/settings/overview': { - id: '/_app/$orgSlug/channels/$channelId/settings/overview' - path: '/overview' - fullPath: '/$orgSlug/channels/$channelId/settings/overview' - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport - parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute - } - '/_app/$orgSlug/channels/$channelId/settings/integrations': { - id: '/_app/$orgSlug/channels/$channelId/settings/integrations' - path: '/integrations' - fullPath: '/$orgSlug/channels/$channelId/settings/integrations' - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport - parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute - } - '/_app/$orgSlug/channels/$channelId/settings/connect': { - id: '/_app/$orgSlug/channels/$channelId/settings/connect' - path: '/connect' - fullPath: '/$orgSlug/channels/$channelId/settings/connect' - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsConnectRouteImport - parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute - } - } +declare module "@tanstack/react-router" { + interface FileRoutesByPath { + "/_dev": { + id: "/_dev" + path: "" + fullPath: "/" + preLoaderRoute: typeof DevLayoutRouteImport + parentRoute: typeof rootRouteImport + } + "/_app": { + id: "/_app" + path: "" + fullPath: "/" + preLoaderRoute: typeof AppLayoutRouteImport + parentRoute: typeof rootRouteImport + } + "/_app/": { + id: "/_app/" + path: "/" + fullPath: "/" + preLoaderRoute: typeof AppIndexRouteImport + parentRoute: typeof AppLayoutRoute + } + "/join/$slug": { + id: "/join/$slug" + path: "/join/$slug" + fullPath: "/join/$slug" + preLoaderRoute: typeof JoinSlugRouteImport + parentRoute: typeof rootRouteImport + } + "/auth/login": { + id: "/auth/login" + path: "/auth/login" + fullPath: "/auth/login" + preLoaderRoute: typeof AuthLoginRouteImport + parentRoute: typeof rootRouteImport + } + "/auth/desktop-login": { + id: "/auth/desktop-login" + path: "/auth/desktop-login" + fullPath: "/auth/desktop-login" + preLoaderRoute: typeof AuthDesktopLoginRouteImport + parentRoute: typeof rootRouteImport + } + "/auth/desktop-callback": { + id: "/auth/desktop-callback" + path: "/auth/desktop-callback" + fullPath: "/auth/desktop-callback" + preLoaderRoute: typeof AuthDesktopCallbackRouteImport + parentRoute: typeof rootRouteImport + } + "/auth/callback": { + id: "/auth/callback" + path: "/auth/callback" + fullPath: "/auth/callback" + preLoaderRoute: typeof AuthCallbackRouteImport + parentRoute: typeof rootRouteImport + } + "/_dev/ui": { + id: "/_dev/ui" + path: "/ui" + fullPath: "/ui" + preLoaderRoute: typeof DevUiLayoutRouteImport + parentRoute: typeof DevLayoutRoute + } + "/_app/$orgSlug": { + id: "/_app/$orgSlug" + path: "/$orgSlug" + fullPath: "/$orgSlug" + preLoaderRoute: typeof AppOrgSlugLayoutRouteImport + parentRoute: typeof AppLayoutRoute + } + "/dev/embeds/": { + id: "/dev/embeds/" + path: "/dev/embeds" + fullPath: "/dev/embeds/" + preLoaderRoute: typeof DevEmbedsIndexRouteImport + parentRoute: typeof rootRouteImport + } + "/_app/select-organization/": { + id: "/_app/select-organization/" + path: "/select-organization" + fullPath: "/select-organization/" + preLoaderRoute: typeof AppSelectOrganizationIndexRouteImport + parentRoute: typeof AppLayoutRoute + } + "/_app/onboarding/": { + id: "/_app/onboarding/" + path: "/onboarding" + fullPath: "/onboarding/" + preLoaderRoute: typeof AppOnboardingIndexRouteImport + parentRoute: typeof AppLayoutRoute + } + "/_app/$orgSlug/": { + id: "/_app/$orgSlug/" + path: "/" + fullPath: "/$orgSlug/" + preLoaderRoute: typeof AppOrgSlugIndexRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/dev/embeds/railway": { + id: "/dev/embeds/railway" + path: "/dev/embeds/railway" + fullPath: "/dev/embeds/railway" + preLoaderRoute: typeof DevEmbedsRailwayRouteImport + parentRoute: typeof rootRouteImport + } + "/dev/embeds/openstatus": { + id: "/dev/embeds/openstatus" + path: "/dev/embeds/openstatus" + fullPath: "/dev/embeds/openstatus" + preLoaderRoute: typeof DevEmbedsOpenstatusRouteImport + parentRoute: typeof rootRouteImport + } + "/dev/embeds/github": { + id: "/dev/embeds/github" + path: "/dev/embeds/github" + fullPath: "/dev/embeds/github" + preLoaderRoute: typeof DevEmbedsGithubRouteImport + parentRoute: typeof rootRouteImport + } + "/dev/embeds/demo": { + id: "/dev/embeds/demo" + path: "/dev/embeds/demo" + fullPath: "/dev/embeds/demo" + preLoaderRoute: typeof DevEmbedsDemoRouteImport + parentRoute: typeof rootRouteImport + } + "/_dev/ui/agent-steps": { + id: "/_dev/ui/agent-steps" + path: "/agent-steps" + fullPath: "/ui/agent-steps" + preLoaderRoute: typeof DevUiAgentStepsRouteImport + parentRoute: typeof DevUiLayoutRoute + } + "/_app/onboarding/setup-organization": { + id: "/_app/onboarding/setup-organization" + path: "/onboarding/setup-organization" + fullPath: "/onboarding/setup-organization" + preLoaderRoute: typeof AppOnboardingSetupOrganizationRouteImport + parentRoute: typeof AppLayoutRoute + } + "/_app/$orgSlug/settings": { + id: "/_app/$orgSlug/settings" + path: "/settings" + fullPath: "/$orgSlug/settings" + preLoaderRoute: typeof AppOrgSlugSettingsLayoutRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/notifications": { + id: "/_app/$orgSlug/notifications" + path: "/notifications" + fullPath: "/$orgSlug/notifications" + preLoaderRoute: typeof AppOrgSlugNotificationsLayoutRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/my-settings": { + id: "/_app/$orgSlug/my-settings" + path: "/my-settings" + fullPath: "/$orgSlug/my-settings" + preLoaderRoute: typeof AppOrgSlugMySettingsLayoutRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/settings/": { + id: "/_app/$orgSlug/settings/" + path: "/" + fullPath: "/$orgSlug/settings/" + preLoaderRoute: typeof AppOrgSlugSettingsIndexRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/notifications/": { + id: "/_app/$orgSlug/notifications/" + path: "/" + fullPath: "/$orgSlug/notifications/" + preLoaderRoute: typeof AppOrgSlugNotificationsIndexRouteImport + parentRoute: typeof AppOrgSlugNotificationsLayoutRoute + } + "/_app/$orgSlug/my-settings/": { + id: "/_app/$orgSlug/my-settings/" + path: "/" + fullPath: "/$orgSlug/my-settings/" + preLoaderRoute: typeof AppOrgSlugMySettingsIndexRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + "/_app/$orgSlug/chat/": { + id: "/_app/$orgSlug/chat/" + path: "/chat" + fullPath: "/$orgSlug/chat/" + preLoaderRoute: typeof AppOrgSlugChatIndexRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/settings/team": { + id: "/_app/$orgSlug/settings/team" + path: "/team" + fullPath: "/$orgSlug/settings/team" + preLoaderRoute: typeof AppOrgSlugSettingsTeamRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/invitations": { + id: "/_app/$orgSlug/settings/invitations" + path: "/invitations" + fullPath: "/$orgSlug/settings/invitations" + preLoaderRoute: typeof AppOrgSlugSettingsInvitationsRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/debug": { + id: "/_app/$orgSlug/settings/debug" + path: "/debug" + fullPath: "/$orgSlug/settings/debug" + preLoaderRoute: typeof AppOrgSlugSettingsDebugRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/custom-emojis": { + id: "/_app/$orgSlug/settings/custom-emojis" + path: "/custom-emojis" + fullPath: "/$orgSlug/settings/custom-emojis" + preLoaderRoute: typeof AppOrgSlugSettingsCustomEmojisRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/connect-invites": { + id: "/_app/$orgSlug/settings/connect-invites" + path: "/connect-invites" + fullPath: "/$orgSlug/settings/connect-invites" + preLoaderRoute: typeof AppOrgSlugSettingsConnectInvitesRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/authentication": { + id: "/_app/$orgSlug/settings/authentication" + path: "/authentication" + fullPath: "/$orgSlug/settings/authentication" + preLoaderRoute: typeof AppOrgSlugSettingsAuthenticationRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/profile/$userId": { + id: "/_app/$orgSlug/profile/$userId" + path: "/profile/$userId" + fullPath: "/$orgSlug/profile/$userId" + preLoaderRoute: typeof AppOrgSlugProfileUserIdRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/notifications/threads": { + id: "/_app/$orgSlug/notifications/threads" + path: "/threads" + fullPath: "/$orgSlug/notifications/threads" + preLoaderRoute: typeof AppOrgSlugNotificationsThreadsRouteImport + parentRoute: typeof AppOrgSlugNotificationsLayoutRoute + } + "/_app/$orgSlug/notifications/general": { + id: "/_app/$orgSlug/notifications/general" + path: "/general" + fullPath: "/$orgSlug/notifications/general" + preLoaderRoute: typeof AppOrgSlugNotificationsGeneralRouteImport + parentRoute: typeof AppOrgSlugNotificationsLayoutRoute + } + "/_app/$orgSlug/notifications/dms": { + id: "/_app/$orgSlug/notifications/dms" + path: "/dms" + fullPath: "/$orgSlug/notifications/dms" + preLoaderRoute: typeof AppOrgSlugNotificationsDmsRouteImport + parentRoute: typeof AppOrgSlugNotificationsLayoutRoute + } + "/_app/$orgSlug/my-settings/profile": { + id: "/_app/$orgSlug/my-settings/profile" + path: "/profile" + fullPath: "/$orgSlug/my-settings/profile" + preLoaderRoute: typeof AppOrgSlugMySettingsProfileRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + "/_app/$orgSlug/my-settings/notifications": { + id: "/_app/$orgSlug/my-settings/notifications" + path: "/notifications" + fullPath: "/$orgSlug/my-settings/notifications" + preLoaderRoute: typeof AppOrgSlugMySettingsNotificationsRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + "/_app/$orgSlug/my-settings/linked-accounts": { + id: "/_app/$orgSlug/my-settings/linked-accounts" + path: "/linked-accounts" + fullPath: "/$orgSlug/my-settings/linked-accounts" + preLoaderRoute: typeof AppOrgSlugMySettingsLinkedAccountsRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + "/_app/$orgSlug/my-settings/desktop": { + id: "/_app/$orgSlug/my-settings/desktop" + path: "/desktop" + fullPath: "/$orgSlug/my-settings/desktop" + preLoaderRoute: typeof AppOrgSlugMySettingsDesktopRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + "/_app/$orgSlug/chat/$id": { + id: "/_app/$orgSlug/chat/$id" + path: "/chat/$id" + fullPath: "/$orgSlug/chat/$id" + preLoaderRoute: typeof AppOrgSlugChatIdRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/settings/integrations": { + id: "/_app/$orgSlug/settings/integrations" + path: "/integrations" + fullPath: "/$orgSlug/settings/integrations" + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/chat-sync": { + id: "/_app/$orgSlug/settings/chat-sync" + path: "/chat-sync" + fullPath: "/$orgSlug/settings/chat-sync" + preLoaderRoute: typeof AppOrgSlugSettingsChatSyncLayoutRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/integrations/": { + id: "/_app/$orgSlug/settings/integrations/" + path: "/" + fullPath: "/$orgSlug/settings/integrations/" + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsIndexRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + "/_app/$orgSlug/settings/chat-sync/": { + id: "/_app/$orgSlug/settings/chat-sync/" + path: "/" + fullPath: "/$orgSlug/settings/chat-sync/" + preLoaderRoute: typeof AppOrgSlugSettingsChatSyncIndexRouteImport + parentRoute: typeof AppOrgSlugSettingsChatSyncLayoutRoute + } + "/_app/$orgSlug/chat/$id/": { + id: "/_app/$orgSlug/chat/$id/" + path: "/" + fullPath: "/$orgSlug/chat/$id/" + preLoaderRoute: typeof AppOrgSlugChatIdIndexRouteImport + parentRoute: typeof AppOrgSlugChatIdRoute + } + "/_app/$orgSlug/settings/integrations/your-apps": { + id: "/_app/$orgSlug/settings/integrations/your-apps" + path: "/your-apps" + fullPath: "/$orgSlug/settings/integrations/your-apps" + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsYourAppsRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + "/_app/$orgSlug/settings/integrations/marketplace": { + id: "/_app/$orgSlug/settings/integrations/marketplace" + path: "/marketplace" + fullPath: "/$orgSlug/settings/integrations/marketplace" + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsMarketplaceRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + "/_app/$orgSlug/settings/integrations/installed": { + id: "/_app/$orgSlug/settings/integrations/installed" + path: "/installed" + fullPath: "/$orgSlug/settings/integrations/installed" + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsInstalledRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + "/_app/$orgSlug/settings/integrations/$integrationId": { + id: "/_app/$orgSlug/settings/integrations/$integrationId" + path: "/$integrationId" + fullPath: "/$orgSlug/settings/integrations/$integrationId" + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + "/_app/$orgSlug/settings/chat-sync/$connectionId": { + id: "/_app/$orgSlug/settings/chat-sync/$connectionId" + path: "/$connectionId" + fullPath: "/$orgSlug/settings/chat-sync/$connectionId" + preLoaderRoute: typeof AppOrgSlugSettingsChatSyncConnectionIdRouteImport + parentRoute: typeof AppOrgSlugSettingsChatSyncLayoutRoute + } + "/_app/$orgSlug/channels/$channelId/settings": { + id: "/_app/$orgSlug/channels/$channelId/settings" + path: "/channels/$channelId/settings" + fullPath: "/$orgSlug/channels/$channelId/settings" + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/chat/$id/files/": { + id: "/_app/$orgSlug/chat/$id/files/" + path: "/files" + fullPath: "/$orgSlug/chat/$id/files/" + preLoaderRoute: typeof AppOrgSlugChatIdFilesIndexRouteImport + parentRoute: typeof AppOrgSlugChatIdRoute + } + "/_app/$orgSlug/channels/$channelId/settings/": { + id: "/_app/$orgSlug/channels/$channelId/settings/" + path: "/" + fullPath: "/$orgSlug/channels/$channelId/settings/" + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsIndexRouteImport + parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute + } + "/_app/$orgSlug/chat/$id/files/media": { + id: "/_app/$orgSlug/chat/$id/files/media" + path: "/files/media" + fullPath: "/$orgSlug/chat/$id/files/media" + preLoaderRoute: typeof AppOrgSlugChatIdFilesMediaRouteImport + parentRoute: typeof AppOrgSlugChatIdRoute + } + "/_app/$orgSlug/channels/$channelId/settings/overview": { + id: "/_app/$orgSlug/channels/$channelId/settings/overview" + path: "/overview" + fullPath: "/$orgSlug/channels/$channelId/settings/overview" + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport + parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute + } + "/_app/$orgSlug/channels/$channelId/settings/integrations": { + id: "/_app/$orgSlug/channels/$channelId/settings/integrations" + path: "/integrations" + fullPath: "/$orgSlug/channels/$channelId/settings/integrations" + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport + parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute + } + "/_app/$orgSlug/channels/$channelId/settings/connect": { + id: "/_app/$orgSlug/channels/$channelId/settings/connect" + path: "/connect" + fullPath: "/$orgSlug/channels/$channelId/settings/connect" + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsConnectRouteImport + parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute + } + } } interface AppOrgSlugMySettingsLayoutRouteChildren { - AppOrgSlugMySettingsDesktopRoute: typeof AppOrgSlugMySettingsDesktopRoute - AppOrgSlugMySettingsLinkedAccountsRoute: typeof AppOrgSlugMySettingsLinkedAccountsRoute - AppOrgSlugMySettingsNotificationsRoute: typeof AppOrgSlugMySettingsNotificationsRoute - AppOrgSlugMySettingsProfileRoute: typeof AppOrgSlugMySettingsProfileRoute - AppOrgSlugMySettingsIndexRoute: typeof AppOrgSlugMySettingsIndexRoute + AppOrgSlugMySettingsDesktopRoute: typeof AppOrgSlugMySettingsDesktopRoute + AppOrgSlugMySettingsLinkedAccountsRoute: typeof AppOrgSlugMySettingsLinkedAccountsRoute + AppOrgSlugMySettingsNotificationsRoute: typeof AppOrgSlugMySettingsNotificationsRoute + AppOrgSlugMySettingsProfileRoute: typeof AppOrgSlugMySettingsProfileRoute + AppOrgSlugMySettingsIndexRoute: typeof AppOrgSlugMySettingsIndexRoute } -const AppOrgSlugMySettingsLayoutRouteChildren: AppOrgSlugMySettingsLayoutRouteChildren = - { - AppOrgSlugMySettingsDesktopRoute: AppOrgSlugMySettingsDesktopRoute, - AppOrgSlugMySettingsLinkedAccountsRoute: - AppOrgSlugMySettingsLinkedAccountsRoute, - AppOrgSlugMySettingsNotificationsRoute: - AppOrgSlugMySettingsNotificationsRoute, - AppOrgSlugMySettingsProfileRoute: AppOrgSlugMySettingsProfileRoute, - AppOrgSlugMySettingsIndexRoute: AppOrgSlugMySettingsIndexRoute, - } +const AppOrgSlugMySettingsLayoutRouteChildren: AppOrgSlugMySettingsLayoutRouteChildren = { + AppOrgSlugMySettingsDesktopRoute: AppOrgSlugMySettingsDesktopRoute, + AppOrgSlugMySettingsLinkedAccountsRoute: AppOrgSlugMySettingsLinkedAccountsRoute, + AppOrgSlugMySettingsNotificationsRoute: AppOrgSlugMySettingsNotificationsRoute, + AppOrgSlugMySettingsProfileRoute: AppOrgSlugMySettingsProfileRoute, + AppOrgSlugMySettingsIndexRoute: AppOrgSlugMySettingsIndexRoute, +} -const AppOrgSlugMySettingsLayoutRouteWithChildren = - AppOrgSlugMySettingsLayoutRoute._addFileChildren( - AppOrgSlugMySettingsLayoutRouteChildren, - ) +const AppOrgSlugMySettingsLayoutRouteWithChildren = AppOrgSlugMySettingsLayoutRoute._addFileChildren( + AppOrgSlugMySettingsLayoutRouteChildren, +) interface AppOrgSlugNotificationsLayoutRouteChildren { - AppOrgSlugNotificationsDmsRoute: typeof AppOrgSlugNotificationsDmsRoute - AppOrgSlugNotificationsGeneralRoute: typeof AppOrgSlugNotificationsGeneralRoute - AppOrgSlugNotificationsThreadsRoute: typeof AppOrgSlugNotificationsThreadsRoute - AppOrgSlugNotificationsIndexRoute: typeof AppOrgSlugNotificationsIndexRoute + AppOrgSlugNotificationsDmsRoute: typeof AppOrgSlugNotificationsDmsRoute + AppOrgSlugNotificationsGeneralRoute: typeof AppOrgSlugNotificationsGeneralRoute + AppOrgSlugNotificationsThreadsRoute: typeof AppOrgSlugNotificationsThreadsRoute + AppOrgSlugNotificationsIndexRoute: typeof AppOrgSlugNotificationsIndexRoute } -const AppOrgSlugNotificationsLayoutRouteChildren: AppOrgSlugNotificationsLayoutRouteChildren = - { - AppOrgSlugNotificationsDmsRoute: AppOrgSlugNotificationsDmsRoute, - AppOrgSlugNotificationsGeneralRoute: AppOrgSlugNotificationsGeneralRoute, - AppOrgSlugNotificationsThreadsRoute: AppOrgSlugNotificationsThreadsRoute, - AppOrgSlugNotificationsIndexRoute: AppOrgSlugNotificationsIndexRoute, - } +const AppOrgSlugNotificationsLayoutRouteChildren: AppOrgSlugNotificationsLayoutRouteChildren = { + AppOrgSlugNotificationsDmsRoute: AppOrgSlugNotificationsDmsRoute, + AppOrgSlugNotificationsGeneralRoute: AppOrgSlugNotificationsGeneralRoute, + AppOrgSlugNotificationsThreadsRoute: AppOrgSlugNotificationsThreadsRoute, + AppOrgSlugNotificationsIndexRoute: AppOrgSlugNotificationsIndexRoute, +} -const AppOrgSlugNotificationsLayoutRouteWithChildren = - AppOrgSlugNotificationsLayoutRoute._addFileChildren( - AppOrgSlugNotificationsLayoutRouteChildren, - ) +const AppOrgSlugNotificationsLayoutRouteWithChildren = AppOrgSlugNotificationsLayoutRoute._addFileChildren( + AppOrgSlugNotificationsLayoutRouteChildren, +) interface AppOrgSlugSettingsChatSyncLayoutRouteChildren { - AppOrgSlugSettingsChatSyncConnectionIdRoute: typeof AppOrgSlugSettingsChatSyncConnectionIdRoute - AppOrgSlugSettingsChatSyncIndexRoute: typeof AppOrgSlugSettingsChatSyncIndexRoute + AppOrgSlugSettingsChatSyncConnectionIdRoute: typeof AppOrgSlugSettingsChatSyncConnectionIdRoute + AppOrgSlugSettingsChatSyncIndexRoute: typeof AppOrgSlugSettingsChatSyncIndexRoute } -const AppOrgSlugSettingsChatSyncLayoutRouteChildren: AppOrgSlugSettingsChatSyncLayoutRouteChildren = - { - AppOrgSlugSettingsChatSyncConnectionIdRoute: - AppOrgSlugSettingsChatSyncConnectionIdRoute, - AppOrgSlugSettingsChatSyncIndexRoute: AppOrgSlugSettingsChatSyncIndexRoute, - } +const AppOrgSlugSettingsChatSyncLayoutRouteChildren: AppOrgSlugSettingsChatSyncLayoutRouteChildren = { + AppOrgSlugSettingsChatSyncConnectionIdRoute: AppOrgSlugSettingsChatSyncConnectionIdRoute, + AppOrgSlugSettingsChatSyncIndexRoute: AppOrgSlugSettingsChatSyncIndexRoute, +} const AppOrgSlugSettingsChatSyncLayoutRouteWithChildren = - AppOrgSlugSettingsChatSyncLayoutRoute._addFileChildren( - AppOrgSlugSettingsChatSyncLayoutRouteChildren, - ) + AppOrgSlugSettingsChatSyncLayoutRoute._addFileChildren(AppOrgSlugSettingsChatSyncLayoutRouteChildren) interface AppOrgSlugSettingsIntegrationsLayoutRouteChildren { - AppOrgSlugSettingsIntegrationsIntegrationIdRoute: typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute - AppOrgSlugSettingsIntegrationsInstalledRoute: typeof AppOrgSlugSettingsIntegrationsInstalledRoute - AppOrgSlugSettingsIntegrationsMarketplaceRoute: typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute - AppOrgSlugSettingsIntegrationsYourAppsRoute: typeof AppOrgSlugSettingsIntegrationsYourAppsRoute - AppOrgSlugSettingsIntegrationsIndexRoute: typeof AppOrgSlugSettingsIntegrationsIndexRoute + AppOrgSlugSettingsIntegrationsIntegrationIdRoute: typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute + AppOrgSlugSettingsIntegrationsInstalledRoute: typeof AppOrgSlugSettingsIntegrationsInstalledRoute + AppOrgSlugSettingsIntegrationsMarketplaceRoute: typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute + AppOrgSlugSettingsIntegrationsYourAppsRoute: typeof AppOrgSlugSettingsIntegrationsYourAppsRoute + AppOrgSlugSettingsIntegrationsIndexRoute: typeof AppOrgSlugSettingsIntegrationsIndexRoute } -const AppOrgSlugSettingsIntegrationsLayoutRouteChildren: AppOrgSlugSettingsIntegrationsLayoutRouteChildren = - { - AppOrgSlugSettingsIntegrationsIntegrationIdRoute: - AppOrgSlugSettingsIntegrationsIntegrationIdRoute, - AppOrgSlugSettingsIntegrationsInstalledRoute: - AppOrgSlugSettingsIntegrationsInstalledRoute, - AppOrgSlugSettingsIntegrationsMarketplaceRoute: - AppOrgSlugSettingsIntegrationsMarketplaceRoute, - AppOrgSlugSettingsIntegrationsYourAppsRoute: - AppOrgSlugSettingsIntegrationsYourAppsRoute, - AppOrgSlugSettingsIntegrationsIndexRoute: - AppOrgSlugSettingsIntegrationsIndexRoute, - } +const AppOrgSlugSettingsIntegrationsLayoutRouteChildren: AppOrgSlugSettingsIntegrationsLayoutRouteChildren = { + AppOrgSlugSettingsIntegrationsIntegrationIdRoute: AppOrgSlugSettingsIntegrationsIntegrationIdRoute, + AppOrgSlugSettingsIntegrationsInstalledRoute: AppOrgSlugSettingsIntegrationsInstalledRoute, + AppOrgSlugSettingsIntegrationsMarketplaceRoute: AppOrgSlugSettingsIntegrationsMarketplaceRoute, + AppOrgSlugSettingsIntegrationsYourAppsRoute: AppOrgSlugSettingsIntegrationsYourAppsRoute, + AppOrgSlugSettingsIntegrationsIndexRoute: AppOrgSlugSettingsIntegrationsIndexRoute, +} const AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren = - AppOrgSlugSettingsIntegrationsLayoutRoute._addFileChildren( - AppOrgSlugSettingsIntegrationsLayoutRouteChildren, - ) + AppOrgSlugSettingsIntegrationsLayoutRoute._addFileChildren( + AppOrgSlugSettingsIntegrationsLayoutRouteChildren, + ) interface AppOrgSlugSettingsLayoutRouteChildren { - AppOrgSlugSettingsChatSyncLayoutRoute: typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren - AppOrgSlugSettingsIntegrationsLayoutRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren - AppOrgSlugSettingsAuthenticationRoute: typeof AppOrgSlugSettingsAuthenticationRoute - AppOrgSlugSettingsConnectInvitesRoute: typeof AppOrgSlugSettingsConnectInvitesRoute - AppOrgSlugSettingsCustomEmojisRoute: typeof AppOrgSlugSettingsCustomEmojisRoute - AppOrgSlugSettingsDebugRoute: typeof AppOrgSlugSettingsDebugRoute - AppOrgSlugSettingsInvitationsRoute: typeof AppOrgSlugSettingsInvitationsRoute - AppOrgSlugSettingsTeamRoute: typeof AppOrgSlugSettingsTeamRoute - AppOrgSlugSettingsIndexRoute: typeof AppOrgSlugSettingsIndexRoute + AppOrgSlugSettingsChatSyncLayoutRoute: typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren + AppOrgSlugSettingsIntegrationsLayoutRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren + AppOrgSlugSettingsAuthenticationRoute: typeof AppOrgSlugSettingsAuthenticationRoute + AppOrgSlugSettingsConnectInvitesRoute: typeof AppOrgSlugSettingsConnectInvitesRoute + AppOrgSlugSettingsCustomEmojisRoute: typeof AppOrgSlugSettingsCustomEmojisRoute + AppOrgSlugSettingsDebugRoute: typeof AppOrgSlugSettingsDebugRoute + AppOrgSlugSettingsInvitationsRoute: typeof AppOrgSlugSettingsInvitationsRoute + AppOrgSlugSettingsTeamRoute: typeof AppOrgSlugSettingsTeamRoute + AppOrgSlugSettingsIndexRoute: typeof AppOrgSlugSettingsIndexRoute } -const AppOrgSlugSettingsLayoutRouteChildren: AppOrgSlugSettingsLayoutRouteChildren = - { - AppOrgSlugSettingsChatSyncLayoutRoute: - AppOrgSlugSettingsChatSyncLayoutRouteWithChildren, - AppOrgSlugSettingsIntegrationsLayoutRoute: - AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren, - AppOrgSlugSettingsAuthenticationRoute: - AppOrgSlugSettingsAuthenticationRoute, - AppOrgSlugSettingsConnectInvitesRoute: - AppOrgSlugSettingsConnectInvitesRoute, - AppOrgSlugSettingsCustomEmojisRoute: AppOrgSlugSettingsCustomEmojisRoute, - AppOrgSlugSettingsDebugRoute: AppOrgSlugSettingsDebugRoute, - AppOrgSlugSettingsInvitationsRoute: AppOrgSlugSettingsInvitationsRoute, - AppOrgSlugSettingsTeamRoute: AppOrgSlugSettingsTeamRoute, - AppOrgSlugSettingsIndexRoute: AppOrgSlugSettingsIndexRoute, - } +const AppOrgSlugSettingsLayoutRouteChildren: AppOrgSlugSettingsLayoutRouteChildren = { + AppOrgSlugSettingsChatSyncLayoutRoute: AppOrgSlugSettingsChatSyncLayoutRouteWithChildren, + AppOrgSlugSettingsIntegrationsLayoutRoute: AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren, + AppOrgSlugSettingsAuthenticationRoute: AppOrgSlugSettingsAuthenticationRoute, + AppOrgSlugSettingsConnectInvitesRoute: AppOrgSlugSettingsConnectInvitesRoute, + AppOrgSlugSettingsCustomEmojisRoute: AppOrgSlugSettingsCustomEmojisRoute, + AppOrgSlugSettingsDebugRoute: AppOrgSlugSettingsDebugRoute, + AppOrgSlugSettingsInvitationsRoute: AppOrgSlugSettingsInvitationsRoute, + AppOrgSlugSettingsTeamRoute: AppOrgSlugSettingsTeamRoute, + AppOrgSlugSettingsIndexRoute: AppOrgSlugSettingsIndexRoute, +} -const AppOrgSlugSettingsLayoutRouteWithChildren = - AppOrgSlugSettingsLayoutRoute._addFileChildren( - AppOrgSlugSettingsLayoutRouteChildren, - ) +const AppOrgSlugSettingsLayoutRouteWithChildren = AppOrgSlugSettingsLayoutRoute._addFileChildren( + AppOrgSlugSettingsLayoutRouteChildren, +) interface AppOrgSlugChatIdRouteChildren { - AppOrgSlugChatIdIndexRoute: typeof AppOrgSlugChatIdIndexRoute - AppOrgSlugChatIdFilesMediaRoute: typeof AppOrgSlugChatIdFilesMediaRoute - AppOrgSlugChatIdFilesIndexRoute: typeof AppOrgSlugChatIdFilesIndexRoute + AppOrgSlugChatIdIndexRoute: typeof AppOrgSlugChatIdIndexRoute + AppOrgSlugChatIdFilesMediaRoute: typeof AppOrgSlugChatIdFilesMediaRoute + AppOrgSlugChatIdFilesIndexRoute: typeof AppOrgSlugChatIdFilesIndexRoute } const AppOrgSlugChatIdRouteChildren: AppOrgSlugChatIdRouteChildren = { - AppOrgSlugChatIdIndexRoute: AppOrgSlugChatIdIndexRoute, - AppOrgSlugChatIdFilesMediaRoute: AppOrgSlugChatIdFilesMediaRoute, - AppOrgSlugChatIdFilesIndexRoute: AppOrgSlugChatIdFilesIndexRoute, + AppOrgSlugChatIdIndexRoute: AppOrgSlugChatIdIndexRoute, + AppOrgSlugChatIdFilesMediaRoute: AppOrgSlugChatIdFilesMediaRoute, + AppOrgSlugChatIdFilesIndexRoute: AppOrgSlugChatIdFilesIndexRoute, } -const AppOrgSlugChatIdRouteWithChildren = - AppOrgSlugChatIdRoute._addFileChildren(AppOrgSlugChatIdRouteChildren) +const AppOrgSlugChatIdRouteWithChildren = AppOrgSlugChatIdRoute._addFileChildren( + AppOrgSlugChatIdRouteChildren, +) interface AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren { - AppOrgSlugChannelsChannelIdSettingsConnectRoute: typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute - AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute: typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute - AppOrgSlugChannelsChannelIdSettingsOverviewRoute: typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute - AppOrgSlugChannelsChannelIdSettingsIndexRoute: typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute + AppOrgSlugChannelsChannelIdSettingsConnectRoute: typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute + AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute: typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute + AppOrgSlugChannelsChannelIdSettingsOverviewRoute: typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute + AppOrgSlugChannelsChannelIdSettingsIndexRoute: typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute } const AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren: AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren = - { - AppOrgSlugChannelsChannelIdSettingsConnectRoute: - AppOrgSlugChannelsChannelIdSettingsConnectRoute, - AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute: - AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute, - AppOrgSlugChannelsChannelIdSettingsOverviewRoute: - AppOrgSlugChannelsChannelIdSettingsOverviewRoute, - AppOrgSlugChannelsChannelIdSettingsIndexRoute: - AppOrgSlugChannelsChannelIdSettingsIndexRoute, - } + { + AppOrgSlugChannelsChannelIdSettingsConnectRoute: AppOrgSlugChannelsChannelIdSettingsConnectRoute, + AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute: + AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute, + AppOrgSlugChannelsChannelIdSettingsOverviewRoute: AppOrgSlugChannelsChannelIdSettingsOverviewRoute, + AppOrgSlugChannelsChannelIdSettingsIndexRoute: AppOrgSlugChannelsChannelIdSettingsIndexRoute, + } const AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren = - AppOrgSlugChannelsChannelIdSettingsLayoutRoute._addFileChildren( - AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren, - ) + AppOrgSlugChannelsChannelIdSettingsLayoutRoute._addFileChildren( + AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren, + ) interface AppOrgSlugLayoutRouteChildren { - AppOrgSlugMySettingsLayoutRoute: typeof AppOrgSlugMySettingsLayoutRouteWithChildren - AppOrgSlugNotificationsLayoutRoute: typeof AppOrgSlugNotificationsLayoutRouteWithChildren - AppOrgSlugSettingsLayoutRoute: typeof AppOrgSlugSettingsLayoutRouteWithChildren - AppOrgSlugIndexRoute: typeof AppOrgSlugIndexRoute - AppOrgSlugChatIdRoute: typeof AppOrgSlugChatIdRouteWithChildren - AppOrgSlugProfileUserIdRoute: typeof AppOrgSlugProfileUserIdRoute - AppOrgSlugChatIndexRoute: typeof AppOrgSlugChatIndexRoute - AppOrgSlugChannelsChannelIdSettingsLayoutRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren + AppOrgSlugMySettingsLayoutRoute: typeof AppOrgSlugMySettingsLayoutRouteWithChildren + AppOrgSlugNotificationsLayoutRoute: typeof AppOrgSlugNotificationsLayoutRouteWithChildren + AppOrgSlugSettingsLayoutRoute: typeof AppOrgSlugSettingsLayoutRouteWithChildren + AppOrgSlugIndexRoute: typeof AppOrgSlugIndexRoute + AppOrgSlugChatIdRoute: typeof AppOrgSlugChatIdRouteWithChildren + AppOrgSlugProfileUserIdRoute: typeof AppOrgSlugProfileUserIdRoute + AppOrgSlugChatIndexRoute: typeof AppOrgSlugChatIndexRoute + AppOrgSlugChannelsChannelIdSettingsLayoutRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren } const AppOrgSlugLayoutRouteChildren: AppOrgSlugLayoutRouteChildren = { - AppOrgSlugMySettingsLayoutRoute: AppOrgSlugMySettingsLayoutRouteWithChildren, - AppOrgSlugNotificationsLayoutRoute: - AppOrgSlugNotificationsLayoutRouteWithChildren, - AppOrgSlugSettingsLayoutRoute: AppOrgSlugSettingsLayoutRouteWithChildren, - AppOrgSlugIndexRoute: AppOrgSlugIndexRoute, - AppOrgSlugChatIdRoute: AppOrgSlugChatIdRouteWithChildren, - AppOrgSlugProfileUserIdRoute: AppOrgSlugProfileUserIdRoute, - AppOrgSlugChatIndexRoute: AppOrgSlugChatIndexRoute, - AppOrgSlugChannelsChannelIdSettingsLayoutRoute: - AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren, + AppOrgSlugMySettingsLayoutRoute: AppOrgSlugMySettingsLayoutRouteWithChildren, + AppOrgSlugNotificationsLayoutRoute: AppOrgSlugNotificationsLayoutRouteWithChildren, + AppOrgSlugSettingsLayoutRoute: AppOrgSlugSettingsLayoutRouteWithChildren, + AppOrgSlugIndexRoute: AppOrgSlugIndexRoute, + AppOrgSlugChatIdRoute: AppOrgSlugChatIdRouteWithChildren, + AppOrgSlugProfileUserIdRoute: AppOrgSlugProfileUserIdRoute, + AppOrgSlugChatIndexRoute: AppOrgSlugChatIndexRoute, + AppOrgSlugChannelsChannelIdSettingsLayoutRoute: + AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren, } -const AppOrgSlugLayoutRouteWithChildren = - AppOrgSlugLayoutRoute._addFileChildren(AppOrgSlugLayoutRouteChildren) +const AppOrgSlugLayoutRouteWithChildren = AppOrgSlugLayoutRoute._addFileChildren( + AppOrgSlugLayoutRouteChildren, +) interface AppLayoutRouteChildren { - AppOrgSlugLayoutRoute: typeof AppOrgSlugLayoutRouteWithChildren - AppIndexRoute: typeof AppIndexRoute - AppOnboardingSetupOrganizationRoute: typeof AppOnboardingSetupOrganizationRoute - AppOnboardingIndexRoute: typeof AppOnboardingIndexRoute - AppSelectOrganizationIndexRoute: typeof AppSelectOrganizationIndexRoute + AppOrgSlugLayoutRoute: typeof AppOrgSlugLayoutRouteWithChildren + AppIndexRoute: typeof AppIndexRoute + AppOnboardingSetupOrganizationRoute: typeof AppOnboardingSetupOrganizationRoute + AppOnboardingIndexRoute: typeof AppOnboardingIndexRoute + AppSelectOrganizationIndexRoute: typeof AppSelectOrganizationIndexRoute } const AppLayoutRouteChildren: AppLayoutRouteChildren = { - AppOrgSlugLayoutRoute: AppOrgSlugLayoutRouteWithChildren, - AppIndexRoute: AppIndexRoute, - AppOnboardingSetupOrganizationRoute: AppOnboardingSetupOrganizationRoute, - AppOnboardingIndexRoute: AppOnboardingIndexRoute, - AppSelectOrganizationIndexRoute: AppSelectOrganizationIndexRoute, + AppOrgSlugLayoutRoute: AppOrgSlugLayoutRouteWithChildren, + AppIndexRoute: AppIndexRoute, + AppOnboardingSetupOrganizationRoute: AppOnboardingSetupOrganizationRoute, + AppOnboardingIndexRoute: AppOnboardingIndexRoute, + AppSelectOrganizationIndexRoute: AppSelectOrganizationIndexRoute, } -const AppLayoutRouteWithChildren = AppLayoutRoute._addFileChildren( - AppLayoutRouteChildren, -) +const AppLayoutRouteWithChildren = AppLayoutRoute._addFileChildren(AppLayoutRouteChildren) interface DevUiLayoutRouteChildren { - DevUiAgentStepsRoute: typeof DevUiAgentStepsRoute + DevUiAgentStepsRoute: typeof DevUiAgentStepsRoute } const DevUiLayoutRouteChildren: DevUiLayoutRouteChildren = { - DevUiAgentStepsRoute: DevUiAgentStepsRoute, + DevUiAgentStepsRoute: DevUiAgentStepsRoute, } -const DevUiLayoutRouteWithChildren = DevUiLayoutRoute._addFileChildren( - DevUiLayoutRouteChildren, -) +const DevUiLayoutRouteWithChildren = DevUiLayoutRoute._addFileChildren(DevUiLayoutRouteChildren) interface DevLayoutRouteChildren { - DevUiLayoutRoute: typeof DevUiLayoutRouteWithChildren + DevUiLayoutRoute: typeof DevUiLayoutRouteWithChildren } const DevLayoutRouteChildren: DevLayoutRouteChildren = { - DevUiLayoutRoute: DevUiLayoutRouteWithChildren, + DevUiLayoutRoute: DevUiLayoutRouteWithChildren, } -const DevLayoutRouteWithChildren = DevLayoutRoute._addFileChildren( - DevLayoutRouteChildren, -) +const DevLayoutRouteWithChildren = DevLayoutRoute._addFileChildren(DevLayoutRouteChildren) const rootRouteChildren: RootRouteChildren = { - AppLayoutRoute: AppLayoutRouteWithChildren, - DevLayoutRoute: DevLayoutRouteWithChildren, - AuthCallbackRoute: AuthCallbackRoute, - AuthDesktopCallbackRoute: AuthDesktopCallbackRoute, - AuthDesktopLoginRoute: AuthDesktopLoginRoute, - AuthLoginRoute: AuthLoginRoute, - JoinSlugRoute: JoinSlugRoute, - DevEmbedsDemoRoute: DevEmbedsDemoRoute, - DevEmbedsGithubRoute: DevEmbedsGithubRoute, - DevEmbedsOpenstatusRoute: DevEmbedsOpenstatusRoute, - DevEmbedsRailwayRoute: DevEmbedsRailwayRoute, - DevEmbedsIndexRoute: DevEmbedsIndexRoute, + AppLayoutRoute: AppLayoutRouteWithChildren, + DevLayoutRoute: DevLayoutRouteWithChildren, + AuthCallbackRoute: AuthCallbackRoute, + AuthDesktopCallbackRoute: AuthDesktopCallbackRoute, + AuthDesktopLoginRoute: AuthDesktopLoginRoute, + AuthLoginRoute: AuthLoginRoute, + JoinSlugRoute: JoinSlugRoute, + DevEmbedsDemoRoute: DevEmbedsDemoRoute, + DevEmbedsGithubRoute: DevEmbedsGithubRoute, + DevEmbedsOpenstatusRoute: DevEmbedsOpenstatusRoute, + DevEmbedsRailwayRoute: DevEmbedsRailwayRoute, + DevEmbedsIndexRoute: DevEmbedsIndexRoute, } -export const routeTree = rootRouteImport - ._addFileChildren(rootRouteChildren) - ._addFileTypes() +export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes() diff --git a/bots/hazel-bot/src/agent-loop.ts b/bots/hazel-bot/src/agent-loop.ts index 8813325dc..783d78da3 100644 --- a/bots/hazel-bot/src/agent-loop.ts +++ b/bots/hazel-bot/src/agent-loop.ts @@ -29,14 +29,12 @@ type AgentError = AiError.AiError | StreamIdleTimeoutError | IterationTimeoutErr export const streamAgentLoop = (options: { prompt: Prompt.RawInput toolkit: Toolkit.WithHandler -}): Stream.Stream< - Response.AnyPart, - AgentError, - LanguageModel.LanguageModel -> => +}): Stream.Stream => Effect.gen(function* () { - const queue: Queue.Queue = - yield* Queue.make() + const queue: Queue.Queue = yield* Queue.make< + Response.AnyPart, + AgentError | Cause.Done + >() yield* Effect.gen(function* () { let currentPrompt = Prompt.make(options.prompt) @@ -87,8 +85,4 @@ export const streamAgentLoop = (options: { }).pipe(Queue.into(queue), Effect.forkScoped) return Stream.fromQueue(queue) - }).pipe(Stream.unwrap) as Stream.Stream< - Response.AnyPart, - AgentError, - LanguageModel.LanguageModel - > + }).pipe(Stream.unwrap) as Stream.Stream diff --git a/bots/hazel-bot/src/degeneration-detector.ts b/bots/hazel-bot/src/degeneration-detector.ts index cb44d7e31..8726f7f1b 100644 --- a/bots/hazel-bot/src/degeneration-detector.ts +++ b/bots/hazel-bot/src/degeneration-detector.ts @@ -33,7 +33,10 @@ export const withDegenerationDetection = ( if (detected) { // Return a sentinel value to trigger failure after mapAccum - return [updated, [{ __degenerate: true, pattern: detected.pattern, repeats: detected.repeats } as any]] as const + return [ + updated, + [{ __degenerate: true, pattern: detected.pattern, repeats: detected.repeats } as any], + ] as const } return [updated, [part]] as const diff --git a/bots/hazel-bot/src/handler.ts b/bots/hazel-bot/src/handler.ts index fa318cfe3..3fd5374e2 100644 --- a/bots/hazel-bot/src/handler.ts +++ b/bots/hazel-bot/src/handler.ts @@ -157,7 +157,9 @@ export const handleAIRequest = (params: { Effect.provideServiceEffect( LanguageModel.LanguageModel, Effect.gen(function* () { - const model = yield* Config.string("AI_MODEL").pipe(Config.withDefault("google/gemini-3-flash-preview")) + const model = yield* Config.string("AI_MODEL").pipe( + Config.withDefault("google/gemini-3-flash-preview"), + ) return yield* makeOpenRouterModel(model) }), ), diff --git a/bots/hazel-bot/src/tools/toolkit.ts b/bots/hazel-bot/src/tools/toolkit.ts index 0008cd189..cc144b7a3 100644 --- a/bots/hazel-bot/src/tools/toolkit.ts +++ b/bots/hazel-bot/src/tools/toolkit.ts @@ -107,7 +107,9 @@ const baseHandlers = { const buildLinearHandlers = (options: { bot: HazelBotClient; orgId: OrganizationId }) => { const getLinearToken = () => - (options.bot as any).integration.getToken(options.orgId, "linear").pipe(Effect.map((r: any) => r.accessToken)) + (options.bot as any).integration + .getToken(options.orgId, "linear") + .pipe(Effect.map((r: any) => r.accessToken)) return { linear_get_account_info: () => diff --git a/bots/linear-bot/src/index.ts b/bots/linear-bot/src/index.ts index ff495bb52..670274422 100644 --- a/bots/linear-bot/src/index.ts +++ b/bots/linear-bot/src/index.ts @@ -120,7 +120,9 @@ ${conversationText}`, const text = response.text // Parse the JSON response - const generatedIssue = yield* Schema.decodeUnknownEffect(GeneratedIssueSchema)(JSON.parse(text)) + const generatedIssue = yield* Schema.decodeUnknownEffect(GeneratedIssueSchema)( + JSON.parse(text), + ) yield* Effect.log(`Generated issue: ${generatedIssue.title}`) diff --git a/libs/bot-sdk/src/hazel-bot-sdk.ts b/libs/bot-sdk/src/hazel-bot-sdk.ts index 8ee302537..7fd6b4895 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.ts @@ -1370,17 +1370,16 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB }, ) => rpc["channel.update"]({ - id: channel.id, - type: channel.type, - organizationId: channel.organizationId, - parentChannelId: channel.parentChannelId, - name: updates.name ?? channel.name, - ...updates, - }) - .pipe( - Effect.map((r: any) => r.data), - Effect.withSpan("bot.channel.update", { attributes: { channelId: channel.id } }), - ), + id: channel.id, + type: channel.type, + organizationId: channel.organizationId, + parentChannelId: channel.parentChannelId, + name: updates.name ?? channel.name, + ...updates, + }).pipe( + Effect.map((r: any) => r.data), + Effect.withSpan("bot.channel.update", { attributes: { channelId: channel.id } }), + ), /** * Ensure a thread exists on a message and return it. @@ -1393,22 +1392,21 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB */ createThread: (messageId: MessageId, channelId: ChannelId) => rpc["channel.createThread"]({ - messageId, - }) - .pipe( - Effect.timeout(Duration.seconds(15)), - Effect.tapCause((cause) => - Effect.logError("[bot.channel.createThread] Failed to ensure thread", { - messageId, - channelId, - cause, - }), - ), - Effect.map((r: any) => r.data), - Effect.withSpan("bot.channel.createThread", { - attributes: { messageId, channelId }, + messageId, + }).pipe( + Effect.timeout(Duration.seconds(15)), + Effect.tapCause((cause) => + Effect.logError("[bot.channel.createThread] Failed to ensure thread", { + messageId, + channelId, + cause, }), ), + Effect.map((r: any) => r.data), + Effect.withSpan("bot.channel.createThread", { + attributes: { messageId, channelId }, + }), + ), }, /** @@ -1422,14 +1420,13 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB */ start: (channelId: ChannelId, memberId: ChannelMemberId) => rpc["typingIndicator.create"]({ - channelId, - memberId, - lastTyped: Date.now(), - }) - .pipe( - Effect.map((r: any) => r.data), - Effect.withSpan("bot.typing.start", { attributes: { channelId, memberId } }), - ), + channelId, + memberId, + lastTyped: Date.now(), + }).pipe( + Effect.map((r: any) => r.data), + Effect.withSpan("bot.typing.start", { attributes: { channelId, memberId } }), + ), /** * Stop showing typing indicator @@ -1437,12 +1434,11 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB */ stop: (id: TypingIndicatorId) => rpc["typingIndicator.delete"]({ - id, - }) - .pipe( - Effect.map((r: any) => r.data), - Effect.withSpan("bot.typing.stop", { attributes: { typingIndicatorId: id } }), - ), + id, + }).pipe( + Effect.map((r: any) => r.data), + Effect.withSpan("bot.typing.stop", { attributes: { typingIndicatorId: id } }), + ), }, /** @@ -1551,9 +1547,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB */ withErrorHandler: (ctx: TypedCommandContext) => - ( - effect: Effect.Effect, - ) => + (effect: Effect.Effect) => effect.pipe( Effect.mapError( (cause) => diff --git a/libs/bot-sdk/src/log-config.ts b/libs/bot-sdk/src/log-config.ts index 29ebc381f..77a2fbb44 100644 --- a/libs/bot-sdk/src/log-config.ts +++ b/libs/bot-sdk/src/log-config.ts @@ -95,10 +95,7 @@ export const debugLogConfig: BotLogConfig = { export const createLoggerLayer = (config: BotLogConfig): Layer.Layer => { const logger = config.format === "structured" ? Logger.consoleStructured : Logger.consolePretty() - return Layer.mergeAll( - Logger.layer([logger]), - Layer.succeed(References.MinimumLogLevel, config.level), - ) + return Layer.mergeAll(Logger.layer([logger]), Layer.succeed(References.MinimumLogLevel, config.level)) } /** diff --git a/package.json b/package.json index a39aeb447..a70ade795 100644 --- a/package.json +++ b/package.json @@ -56,4 +56,4 @@ }, "packageManager": "bun@1.3.10", "trustedDependencies": [] -} \ No newline at end of file +} diff --git a/packages/domain/src/errors.ts b/packages/domain/src/errors.ts index c9f0c0782..413c89f1d 100644 --- a/packages/domain/src/errors.ts +++ b/packages/domain/src/errors.ts @@ -124,13 +124,11 @@ export function withRemapDbErrors( return effect.pipe( Effect.catchIf( - (e): e is Extract => - Predicate.isTagged(e, "DatabaseError"), + (e): e is Extract => Predicate.isTagged(e, "DatabaseError"), (err) => toInternalError(err, "There was a database error when"), ), Effect.catchIf( - (e): e is Extract => - Predicate.isTagged(e, "SchemaError"), + (e): e is Extract => Predicate.isTagged(e, "SchemaError"), (err) => toInternalError(err, "There was an error in parsing when"), ), ) as Effect.Effect | InternalServerError, A> From 6bea33520c0b2663496bb70c14fb47800d6412c7 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 14:23:25 +0100 Subject: [PATCH 20/34] fix --- apps/backend/src/index.ts | 4 +- .../src/policies/policy-test-helpers.ts | 2 +- .../src/routes/integration-resources.http.ts | 38 +- apps/backend/src/routes/integrations.http.ts | 24 +- apps/backend/src/routes/internal.http.ts | 4 +- .../src/rpc/handlers/channel-sections.ts | 3 +- .../src/rpc/handlers/channel-webhooks.ts | 3 +- .../src/rpc/handlers/connect-shares.ts | 2 +- .../src/rpc/handlers/pinned-messages.ts | 3 +- apps/backend/src/rpc/middleware/auth.ts | 8 +- .../src/rpc/middleware/scope-injection.ts | 8 +- apps/backend/src/scripts/sync-workos.ts | 2 +- apps/backend/src/services/auth.ts | 9 +- .../chat-sync-core-worker.integration.test.ts | 231 +- .../chat-sync/chat-sync-core-worker.ts | 4229 ++++++++--------- .../chat-sync/chat-sync-provider-registry.ts | 8 +- .../chat-sync/discord-gateway-service.ts | 4 +- .../services/chat-sync/discord-sync-worker.ts | 311 +- .../src/services/integration-token-service.ts | 4 +- .../services/message-side-effect-service.ts | 4 +- .../services/oauth/oauth-provider-registry.ts | 2 +- .../oauth/providers/discord-oauth-provider.ts | 7 +- 22 files changed, 2458 insertions(+), 2452 deletions(-) diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 7eaa81a34..46c3d1293 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -70,7 +70,7 @@ import { DatabaseLive } from "./services/database" import { IntegrationTokenService } from "./services/integration-token-service" import { IntegrationBotService } from "./services/integrations/integration-bot-service" import { ChatSyncAttributionReconciler } from "./services/chat-sync/chat-sync-attribution-reconciler" -import { DiscordSyncWorker } from "./services/chat-sync/discord-sync-worker" +import { DiscordSyncWorkerLayer } from "./services/chat-sync/discord-sync-worker" import { DiscordGatewayService } from "./services/chat-sync/discord-gateway-service" import { MessageOutboxDispatcher } from "./services/message-outbox-dispatcher" import { MessageSideEffectService } from "./services/message-side-effect-service" @@ -202,7 +202,7 @@ const MainLive = Layer.mergeAll( OAuthProviderRegistry.layer, IntegrationBotService.layer, ChatSyncAttributionReconciler.layer, - DiscordSyncWorker.layer, + DiscordSyncWorkerLayer, DiscordGatewayService.layer, MessageSideEffectService.layer, MessageOutboxDispatcher.layer, diff --git a/apps/backend/src/policies/policy-test-helpers.ts b/apps/backend/src/policies/policy-test-helpers.ts index b134c4278..b52865394 100644 --- a/apps/backend/src/policies/policy-test-helpers.ts +++ b/apps/backend/src/policies/policy-test-helpers.ts @@ -85,7 +85,7 @@ const emptyMessageRepoLayer = Layer.succeed(MessageRepo, { * Provides stub repos for channels, channel members, and messages. */ export const makeOrgResolverLayer = (members: Record) => - OrgResolver.DefaultWithoutDependencies.pipe( + OrgResolver.layer.pipe( Layer.provide(makeOrganizationMemberRepoLayer(members)), Layer.provide(emptyChannelRepoLayer), Layer.provide(emptyChannelMemberRepoLayer), diff --git a/apps/backend/src/routes/integration-resources.http.ts b/apps/backend/src/routes/integration-resources.http.ts index 13048bf0d..70121afc4 100644 --- a/apps/backend/src/routes/integration-resources.http.ts +++ b/apps/backend/src/routes/integration-resources.http.ts @@ -351,10 +351,11 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( */ const handleGetGitHubRepositories = Effect.fn("integration-resources.getGitHubRepositories")(function* ( path: { orgId: OrganizationId }, - query: { page: number; perPage: number }, + query: { page?: number; perPage?: number }, ) { - const { orgId } = params - const { page, perPage } = query + const { orgId } = path + const page = query.page ?? 1 + const perPage = query.perPage ?? 30 const connectionRepo = yield* IntegrationConnectionRepo const tokenService = yield* IntegrationTokenService @@ -430,13 +431,13 @@ const getActiveDiscordConnection = Effect.fn("integration-resources.getActiveDis const handleGetDiscordGuilds = Effect.fn("integration-resources.getDiscordGuilds")(function* (path: { orgId: OrganizationId }) { - const { orgId } = params + const { orgId } = path const tokenService = yield* IntegrationTokenService const connection = yield* getActiveDiscordConnection(orgId) const accessToken = yield* tokenService.getValidAccessToken(connection.id) - const guilds = yield* Discord.DiscordApiClient.listGuilds(accessToken).pipe( - Effect.provide(Discord.DiscordApiClient.layer), + const discordApiClient = yield* Discord.DiscordApiClient + const guilds = yield* discordApiClient.listGuilds(accessToken).pipe( Effect.mapError( (error) => new IntegrationResourceError({ @@ -452,24 +453,12 @@ const handleGetDiscordGuilds = Effect.fn("integration-resources.getDiscordGuilds const handleGetDiscordGuildChannels = Effect.fn("integration-resources.getDiscordGuildChannels")( function* (path: { orgId: OrganizationId; guildId: string }) { - const { orgId, guildId } = params + const { orgId, guildId } = path yield* getActiveDiscordConnection(orgId) - const botToken = yield* Config.redacted("DISCORD_BOT_TOKEN").pipe( - Effect.mapError( - (error) => - new InternalServerError({ - message: "Discord bot token is not configured", - detail: String(error), - }), - ), - ) - - const channels = yield* Discord.DiscordApiClient.listGuildChannels( - guildId, - Redacted.value(botToken), - ).pipe( - Effect.provide(Discord.DiscordApiClient.layer), + const botToken = yield* Config.redacted("DISCORD_BOT_TOKEN") + const discordApiClient = yield* Discord.DiscordApiClient + const channels = yield* discordApiClient.listGuildChannels(guildId, Redacted.value(botToken)).pipe( Effect.mapError( (error) => new IntegrationResourceError({ @@ -481,7 +470,10 @@ const handleGetDiscordGuildChannels = Effect.fn("integration-resources.getDiscor ) return new DiscordGuildChannelsResponse({ - channels: channels.map((c) => ({ ...c, id: c.id as ExternalChannelId })), + channels: channels.map((c: { id: string; name: string; type: number }) => ({ + ...c, + id: c.id as ExternalChannelId, + })), }) }, ) diff --git a/apps/backend/src/routes/integrations.http.ts b/apps/backend/src/routes/integrations.http.ts index 75c1bde64..2a220cef6 100644 --- a/apps/backend/src/routes/integrations.http.ts +++ b/apps/backend/src/routes/integrations.http.ts @@ -913,7 +913,8 @@ const handleOAuthCallback = Effect.fn("integrations.oauthCallback")(function* ( if (parsedState.level === "organization") { // Add seeded bot to org for org-level OAuth integration providers. - yield* IntegrationBotService.addBotToOrg(provider, parsedState.organizationId as OrganizationId).pipe( + const integrationBotService = yield* IntegrationBotService + yield* integrationBotService.addBotToOrg(provider, parsedState.organizationId as OrganizationId).pipe( Effect.tap((result) => Option.isSome(result) ? Effect.logInfo("Integration bot added to organization", { @@ -975,9 +976,10 @@ const handleConnectApiKey = Effect.fn("integrations.connectApiKey")(function* ( if (provider === "craft") { validatedBaseUrl = yield* validateCraftBaseUrl(baseUrl) const parsedBaseUrl = new URL(validatedBaseUrl) - const spaceInfo = yield* CraftApiClient.getSpaceInfo(validatedBaseUrl, token).pipe( + const craftApiClient = yield* CraftApiClient + const spaceInfo = yield* craftApiClient.getSpaceInfo(validatedBaseUrl, token).pipe( Effect.catchTags({ - CraftApiError: (error) => + CraftApiError: (error: CraftApiError) => Effect.logWarning("Craft API key validation failed", { event: "craft_api_key_validation_failed", provider, @@ -986,7 +988,7 @@ const handleConnectApiKey = Effect.fn("integrations.connectApiKey")(function* ( baseUrlPath: parsedBaseUrl.pathname, ...craftConnectApiKeyErrorLogFields(error), }).pipe(Effect.andThen(Effect.fail(mapCraftConnectApiKeyError(error)))), - CraftNotFoundError: (error) => + CraftNotFoundError: (error: CraftNotFoundError) => Effect.logWarning("Craft API key validation failed", { event: "craft_api_key_validation_failed", provider, @@ -995,7 +997,7 @@ const handleConnectApiKey = Effect.fn("integrations.connectApiKey")(function* ( baseUrlPath: parsedBaseUrl.pathname, ...craftConnectApiKeyErrorLogFields(error), }).pipe(Effect.andThen(Effect.fail(mapCraftConnectApiKeyError(error)))), - CraftRateLimitError: (error) => + CraftRateLimitError: (error: CraftRateLimitError) => Effect.logWarning("Craft API key validation failed", { event: "craft_api_key_validation_failed", provider, @@ -1047,7 +1049,8 @@ const handleConnectApiKey = Effect.fn("integrations.connectApiKey")(function* ( }) // Best-effort: add integration bot to org - yield* IntegrationBotService.addBotToOrg(provider, orgId).pipe( + const integrationBotService = yield* IntegrationBotService + yield* integrationBotService.addBotToOrg(provider, orgId).pipe( Effect.catch((error) => Effect.logWarning("Failed to add integration bot to org (non-critical)", { event: "integration_bot_add_failed", @@ -1240,21 +1243,24 @@ export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations" .handle("connectApiKey", ({ params, payload }) => handleConnectApiKey(params, payload).pipe( Effect.catchTags({ - DatabaseError: (error) => + DatabaseError: (error: { readonly _tag: "DatabaseError"; readonly message?: string }) => Effect.fail( new InternalServerError({ message: "Database error during API key connection", detail: String(error), }), ), - SchemaError: (error) => + SchemaError: (error: { readonly _tag: "SchemaError"; readonly message?: string }) => Effect.fail( new InternalServerError({ message: "Failed to parse API response", detail: String(error), }), ), - IntegrationEncryptionError: (error) => + IntegrationEncryptionError: (error: { + readonly _tag: "IntegrationEncryptionError" + readonly message?: string + }) => Effect.fail( new InternalServerError({ message: "Failed to encrypt token", diff --git a/apps/backend/src/routes/internal.http.ts b/apps/backend/src/routes/internal.http.ts index def567b64..a66175dd2 100644 --- a/apps/backend/src/routes/internal.http.ts +++ b/apps/backend/src/routes/internal.http.ts @@ -22,7 +22,9 @@ export const HttpInternalLive = HttpApiBuilder.group(HazelApi, "internal", (hand const request = yield* HttpServerRequest.HttpServerRequest // Optionally verify internal secret for server-to-server auth - const internalSecretOption = yield* Config.string("INTERNAL_SECRET").pipe(Config.option) + const internalSecretOption = yield* Effect.config( + Config.string("INTERNAL_SECRET").pipe(Config.option), + ) const internalSecret = Option.getOrUndefined(internalSecretOption) if (internalSecret) { diff --git a/apps/backend/src/rpc/handlers/channel-sections.ts b/apps/backend/src/rpc/handlers/channel-sections.ts index f277c9b9a..d4fbfa113 100644 --- a/apps/backend/src/rpc/handlers/channel-sections.ts +++ b/apps/backend/src/rpc/handlers/channel-sections.ts @@ -16,6 +16,7 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( const channelSectionPolicy = yield* ChannelSectionPolicy const channelRepo = yield* ChannelRepo const channelSectionRepo = yield* ChannelSectionRepo + const orgResolver = yield* OrgResolver return { "channelSection.create": ({ id, ...payload }) => @@ -170,7 +171,7 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( "moveChannel", )( withAnnotatedScope((scope) => - OrgResolver.requireAdminOrOwner( + orgResolver.requireAdminOrOwner( channel.value.organizationId, scope, "ChannelSection", diff --git a/apps/backend/src/rpc/handlers/channel-webhooks.ts b/apps/backend/src/rpc/handlers/channel-webhooks.ts index 77a5d0253..c89db7c17 100644 --- a/apps/backend/src/rpc/handlers/channel-webhooks.ts +++ b/apps/backend/src/rpc/handlers/channel-webhooks.ts @@ -43,6 +43,7 @@ export const ChannelWebhookRpcLive = ChannelWebhookRpcs.toLayer( Effect.gen(function* () { const db = yield* Database.Database const channelWebhookPolicy = yield* ChannelWebhookPolicy + const orgResolver = yield* OrgResolver return { "channelWebhook.create": (payload) => @@ -246,7 +247,7 @@ export const ChannelWebhookRpcLive = ChannelWebhookRpcs.toLayer( "list", )( withAnnotatedScope((scope) => - OrgResolver.requireScope(organizationId, scope, "ChannelWebhook", "list"), + orgResolver.requireScope(organizationId, scope, "ChannelWebhook", "list"), ), ) diff --git a/apps/backend/src/rpc/handlers/connect-shares.ts b/apps/backend/src/rpc/handlers/connect-shares.ts index a259df8b3..158b65528 100644 --- a/apps/backend/src/rpc/handlers/connect-shares.ts +++ b/apps/backend/src/rpc/handlers/connect-shares.ts @@ -165,7 +165,7 @@ export const remapGuestMountInsertConflict = ({ function remapPermissionError( make: Effect.Effect, ): Effect.Effect | UnauthorizedError, R> { - return Effect.catchIf(effect, PermissionError.is, (err) => + return Effect.catchIf(make, PermissionError.is, (err) => Effect.fail( new UnauthorizedError({ message: err.message, diff --git a/apps/backend/src/rpc/handlers/pinned-messages.ts b/apps/backend/src/rpc/handlers/pinned-messages.ts index a05601c80..328a84e2d 100644 --- a/apps/backend/src/rpc/handlers/pinned-messages.ts +++ b/apps/backend/src/rpc/handlers/pinned-messages.ts @@ -1,9 +1,10 @@ -import { PinnedMessageRepo } from "@hazel/backend-core" +import { MessageRepo, PinnedMessageRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { CurrentUser, withRemapDbErrors } from "@hazel/domain" import { PinnedMessageRpcs } from "@hazel/domain/rpc" import { Effect } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" +import { MessagePolicy } from "../../policies/message-policy" import { PinnedMessagePolicy } from "../../policies/pinned-message-policy" /** diff --git a/apps/backend/src/rpc/middleware/auth.ts b/apps/backend/src/rpc/middleware/auth.ts index c2cf48ed3..71f01501c 100644 --- a/apps/backend/src/rpc/middleware/auth.ts +++ b/apps/backend/src/rpc/middleware/auth.ts @@ -1,6 +1,6 @@ import { Headers } from "effect/unstable/http" import { BotRepo, UserRepo } from "@hazel/backend-core" -import { InvalidBearerTokenError, type CurrentUser, SessionNotProvidedError } from "@hazel/domain" +import { CurrentUser, InvalidBearerTokenError, SessionNotProvidedError } from "@hazel/domain" import { Effect, Layer, Option } from "effect" import { AuthMiddleware } from "@hazel/domain/rpc" import { type ApiScope, CurrentBotScopes } from "@hazel/domain/scopes" @@ -34,7 +34,7 @@ export const AuthMiddlewareLive = Layer.effect( const botRepo = yield* BotRepo const userRepo = yield* UserRepo - return AuthMiddleware.of(({ headers }) => + return AuthMiddleware.of((effect, { headers }) => Effect.gen(function* () { // Check for Bearer token first (bot SDK or desktop app authentication) const authHeader = Headers.get(headers, "authorization") @@ -46,7 +46,7 @@ export const AuthMiddlewareLive = Layer.effect( if (isJwtToken(token)) { // Authenticate using WorkOS JWT const currentUser = yield* sessionManager.authenticateWithBearer(token) - return currentUser + return yield* Effect.provideService(effect, CurrentUser.Context, currentUser) } // Otherwise, treat as bot token (hash-based lookup) @@ -115,7 +115,7 @@ export const AuthMiddlewareLive = Layer.effect( settings: user.settings, } - return botUser + return yield* Effect.provideService(effect, CurrentUser.Context, botUser) } // No valid authentication provided diff --git a/apps/backend/src/rpc/middleware/scope-injection.ts b/apps/backend/src/rpc/middleware/scope-injection.ts index eddb5cb12..112b97c18 100644 --- a/apps/backend/src/rpc/middleware/scope-injection.ts +++ b/apps/backend/src/rpc/middleware/scope-injection.ts @@ -11,11 +11,11 @@ import { CurrentRpcScopes } from "@hazel/domain/scopes" */ export const ScopeInjectionMiddlewareLive = Layer.succeed( ScopeInjectionMiddleware, - ScopeInjectionMiddleware.of(({ rpc, next }) => { - const scopesOption = ServiceMap.get(rpc.annotations, RequiredScopes) + ScopeInjectionMiddleware.of((effect, { rpc }) => { + const scopesOption = ServiceMap.getOption(rpc.annotations, RequiredScopes) if (Option.isNone(scopesOption)) { - return next + return effect } - return Effect.provideService(next, CurrentRpcScopes, scopesOption.value) + return Effect.provideService(effect, CurrentRpcScopes, scopesOption.value) }), ) diff --git a/apps/backend/src/scripts/sync-workos.ts b/apps/backend/src/scripts/sync-workos.ts index 216b10af1..c50f358c1 100644 --- a/apps/backend/src/scripts/sync-workos.ts +++ b/apps/backend/src/scripts/sync-workos.ts @@ -25,6 +25,6 @@ const syncWorkos = Effect.gen(function* () { const workOsSync = yield* WorkOSSync yield* workOsSync.syncAll -}).pipe(Effect.provide(MainLive), Effect.provide(Logger.pretty)) +}).pipe(Effect.provide(MainLive), Effect.provide(Logger.prettyLayer())) Effect.runPromise(syncWorkos) diff --git a/apps/backend/src/services/auth.ts b/apps/backend/src/services/auth.ts index f03c22fd8..14f4c172b 100644 --- a/apps/backend/src/services/auth.ts +++ b/apps/backend/src/services/auth.ts @@ -9,14 +9,15 @@ export const AuthorizationLive = Layer.effect( const sessionManager = yield* SessionManager - return { - bearer: (bearerToken) => + return CurrentUser.Authorization.of({ + bearer: (httpEffect, { credential: bearerToken }) => Effect.gen(function* () { yield* Effect.logDebug("checking bearer token") // Use SessionManager to handle bearer token authentication - return yield* sessionManager.authenticateWithBearer(Redacted.value(bearerToken)) + const user = yield* sessionManager.authenticateWithBearer(Redacted.value(bearerToken)) + return yield* Effect.provideService(httpEffect, CurrentUser.Context, user) }), - } + }) }), ) diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.integration.test.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.integration.test.ts index 0a96101b5..c9e012675 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.integration.test.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.integration.test.ts @@ -35,7 +35,7 @@ import { recordChatSyncDiagnostic } from "../../test/chat-sync-test-diagnostics" import { ChannelAccessSyncService } from "../channel-access-sync" import { IntegrationBotService } from "../integrations/integration-bot-service" import { ChatSyncProviderRegistry, type ChatSyncProviderAdapter } from "./chat-sync-provider-registry" -import { ChatSyncCoreWorker } from "./chat-sync-core-worker" +import { ChatSyncCoreWorker, ChatSyncCoreWorkerMake } from "./chat-sync-core-worker" type SeedContext = { organizationId: OrganizationId @@ -369,16 +369,16 @@ const makeWorkerLayer = ( repoLayer, Layer.succeed(ChatSyncProviderRegistry, { getAdapter: () => Effect.succeed(params.providerAdapter), - } as unknown as ChatSyncProviderRegistry), + } as never), Layer.succeed(IntegrationBotService, { getOrCreateBotUser: () => Effect.succeed({ id: params.botUserId }), - } as unknown as IntegrationBotService), + } as never), Layer.succeed(ChannelAccessSyncService, { syncChannel: (channelId: ChannelId) => Effect.sync(() => { params.onSyncChannel?.(channelId) }), - } as unknown as ChannelAccessSyncService), + } as never), Layer.succeed(Discord.DiscordApiClient, { createWebhook: () => Effect.fail(new Error("not used in deterministic integration tests")), executeWebhookMessage: () => @@ -391,10 +391,10 @@ const makeWorkerLayer = ( addReaction: () => Effect.fail(new Error("not used in deterministic integration tests")), removeReaction: () => Effect.fail(new Error("not used in deterministic integration tests")), createThread: () => Effect.fail(new Error("not used in deterministic integration tests")), - } as unknown as Discord.DiscordApiClient), + } as never), ) - return ChatSyncCoreWorker.DefaultWithoutDependencies.pipe(Layer.provide(deps)) + return Layer.effect(ChatSyncCoreWorker, ChatSyncCoreWorkerMake).pipe(Layer.provide(deps)) } describe("ChatSyncCoreWorker integration", () => { @@ -436,9 +436,10 @@ describe("ChatSyncCoreWorker integration", () => { }) const createResult = await runEffect( - ChatSyncCoreWorker.syncHazelMessageToProvider(syncConnectionId, messageId).pipe( - Effect.provide(workerLayer), - ), + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.syncHazelMessageToProvider(syncConnectionId, messageId) + }).pipe(Effect.provide(workerLayer)), ) recordChatSyncDiagnostic({ suite: "chat-sync-core-worker.integration", @@ -453,9 +454,10 @@ describe("ChatSyncCoreWorker integration", () => { expect(recorder.calls.createMessage).toHaveLength(1) const dedupedResult = await runEffect( - ChatSyncCoreWorker.syncHazelMessageToProvider(syncConnectionId, messageId).pipe( - Effect.provide(workerLayer), - ), + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.syncHazelMessageToProvider(syncConnectionId, messageId) + }).pipe(Effect.provide(workerLayer)), ) expect(dedupedResult.status).toBe("deduped") @@ -472,17 +474,19 @@ describe("ChatSyncCoreWorker integration", () => { ) const updateResult = await runEffect( - ChatSyncCoreWorker.syncHazelMessageUpdateToProvider(syncConnectionId, messageId).pipe( - Effect.provide(workerLayer), - ), + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.syncHazelMessageUpdateToProvider(syncConnectionId, messageId) + }).pipe(Effect.provide(workerLayer)), ) expect(updateResult.status).toBe("updated") expect(recorder.calls.updateMessage).toHaveLength(1) const deleteResult = await runEffect( - ChatSyncCoreWorker.syncHazelMessageDeleteToProvider(syncConnectionId, messageId).pipe( - Effect.provide(workerLayer), - ), + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.syncHazelMessageDeleteToProvider(syncConnectionId, messageId) + }).pipe(Effect.provide(workerLayer)), ) expect(deleteResult.status).toBe("deleted") expect(recorder.calls.deleteMessage).toHaveLength(1) @@ -538,9 +542,10 @@ describe("ChatSyncCoreWorker integration", () => { content: "hello", }) const createMessageResult = await runEffect( - ChatSyncCoreWorker.syncHazelMessageToProvider(syncConnectionId, messageId).pipe( - Effect.provide(workerLayer), - ), + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.syncHazelMessageToProvider(syncConnectionId, messageId) + }).pipe(Effect.provide(workerLayer)), ) expect(createMessageResult.status).toBe("synced") const externalMessageId = createMessageResult.externalMessageId @@ -566,19 +571,23 @@ describe("ChatSyncCoreWorker integration", () => { }) const createReactionResult = await runEffect( - ChatSyncCoreWorker.syncHazelReactionCreateToProvider(syncConnectionId, reactionId).pipe( - Effect.provide(workerLayer), - ), + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.syncHazelReactionCreateToProvider(syncConnectionId, reactionId) + }).pipe(Effect.provide(workerLayer)), ) expect(createReactionResult.status).toBe("created") expect(recorder.calls.addReaction).toHaveLength(1) const ignoredDelete = await runEffect( - ChatSyncCoreWorker.syncHazelReactionDeleteToProvider(syncConnectionId, { - hazelChannelId: ctx.channelId, - hazelMessageId: messageId, - emoji: "🔥", - userId: ctx.authorUserId, + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.syncHazelReactionDeleteToProvider(syncConnectionId, { + hazelChannelId: ctx.channelId, + hazelMessageId: messageId, + emoji: "🔥", + userId: ctx.authorUserId, + }) }).pipe(Effect.provide(workerLayer)), ) expect(ignoredDelete.status).toBe("ignored_remaining_reactions") @@ -595,16 +604,19 @@ describe("ChatSyncCoreWorker integration", () => { ) const deleteReactionResult = await runEffect( - ChatSyncCoreWorker.syncHazelReactionDeleteToProvider( - syncConnectionId, - { - hazelChannelId: ctx.channelId, - hazelMessageId: messageId, - emoji: "🔥", - userId: ctx.authorUserId, - }, - "hazel:reaction:delete:second-pass", - ).pipe(Effect.provide(workerLayer)), + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.syncHazelReactionDeleteToProvider( + syncConnectionId, + { + hazelChannelId: ctx.channelId, + hazelMessageId: messageId, + emoji: "🔥", + userId: ctx.authorUserId, + }, + "hazel:reaction:delete:second-pass", + ) + }).pipe(Effect.provide(workerLayer)), ) expect(deleteReactionResult.status).toBe("deleted") expect(recorder.calls.removeReaction).toHaveLength(1) @@ -632,14 +644,17 @@ describe("ChatSyncCoreWorker integration", () => { }) const ingressCreate = await runEffect( - ChatSyncCoreWorker.ingestMessageCreate({ - syncConnectionId, - externalChannelId: externalParentChannelId, - externalMessageId: "22222222222222221" as ExternalMessageId, - externalAuthorId: "ext-user-1" as ExternalUserId, - externalAuthorDisplayName: "External User", - content: "from discord", - dedupeKey: "ext:create:1", + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.ingestMessageCreate({ + syncConnectionId, + externalChannelId: externalParentChannelId, + externalMessageId: "22222222222222221" as ExternalMessageId, + externalAuthorId: "ext-user-1" as ExternalUserId, + externalAuthorDisplayName: "External User", + content: "from discord", + dedupeKey: "ext:create:1", + }) }).pipe(Effect.provide(workerLayer)), ) expect(ingressCreate.status).toBe("created") @@ -649,62 +664,77 @@ describe("ChatSyncCoreWorker integration", () => { const hazelMessageId = ingressCreate.hazelMessageId const ingressUpdate = await runEffect( - ChatSyncCoreWorker.ingestMessageUpdate({ - syncConnectionId, - externalChannelId: externalParentChannelId, - externalMessageId: "22222222222222221" as ExternalMessageId, - content: "from discord updated", - dedupeKey: "ext:update:1", + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.ingestMessageUpdate({ + syncConnectionId, + externalChannelId: externalParentChannelId, + externalMessageId: "22222222222222221" as ExternalMessageId, + content: "from discord updated", + dedupeKey: "ext:update:1", + }) }).pipe(Effect.provide(workerLayer)), ) expect(ingressUpdate.status).toBe("updated") expect(ingressUpdate.hazelMessageId).toBe(hazelMessageId) const reactionAdd = await runEffect( - ChatSyncCoreWorker.ingestReactionAdd({ - syncConnectionId, - externalChannelId: externalParentChannelId, - externalMessageId: "22222222222222221" as ExternalMessageId, - externalUserId: "ext-user-2" as ExternalUserId, - externalAuthorDisplayName: "React User", - emoji: "🔥", - dedupeKey: "ext:reaction:add:1", + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.ingestReactionAdd({ + syncConnectionId, + externalChannelId: externalParentChannelId, + externalMessageId: "22222222222222221" as ExternalMessageId, + externalUserId: "ext-user-2" as ExternalUserId, + externalAuthorDisplayName: "React User", + emoji: "🔥", + dedupeKey: "ext:reaction:add:1", + }) }).pipe(Effect.provide(workerLayer)), ) expect(reactionAdd.status).toBe("created") const reactionDelete = await runEffect( - ChatSyncCoreWorker.ingestReactionRemove({ - syncConnectionId, - externalChannelId: externalParentChannelId, - externalMessageId: "22222222222222221" as ExternalMessageId, - externalUserId: "ext-user-2" as ExternalUserId, - externalAuthorDisplayName: "React User", - emoji: "🔥", - dedupeKey: "ext:reaction:remove:1", + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.ingestReactionRemove({ + syncConnectionId, + externalChannelId: externalParentChannelId, + externalMessageId: "22222222222222221" as ExternalMessageId, + externalUserId: "ext-user-2" as ExternalUserId, + externalAuthorDisplayName: "React User", + emoji: "🔥", + dedupeKey: "ext:reaction:remove:1", + }) }).pipe(Effect.provide(workerLayer)), ) expect(reactionDelete.status).toBe("deleted") const threadCreate = await runEffect( - ChatSyncCoreWorker.ingestThreadCreate({ - syncConnectionId, - externalParentChannelId, - externalThreadId: "33333333333333331" as ExternalThreadId, - externalRootMessageId: "22222222222222221" as ExternalMessageId, - name: "Thread From Discord", - dedupeKey: "ext:thread:create:1", + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.ingestThreadCreate({ + syncConnectionId, + externalParentChannelId, + externalThreadId: "33333333333333331" as ExternalThreadId, + externalRootMessageId: "22222222222222221" as ExternalMessageId, + name: "Thread From Discord", + dedupeKey: "ext:thread:create:1", + }) }).pipe(Effect.provide(workerLayer)), ) expect(threadCreate.status).toBe("created") expect(syncedChannels).toContain(threadCreate.hazelThreadChannelId) const ingressDelete = await runEffect( - ChatSyncCoreWorker.ingestMessageDelete({ - syncConnectionId, - externalChannelId: externalParentChannelId, - externalMessageId: "22222222222222221" as ExternalMessageId, - dedupeKey: "ext:delete:1", + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.ingestMessageDelete({ + syncConnectionId, + externalChannelId: externalParentChannelId, + externalMessageId: "22222222222222221" as ExternalMessageId, + dedupeKey: "ext:delete:1", + }) }).pipe(Effect.provide(workerLayer)), ) expect(ingressDelete.status).toBe("deleted") @@ -783,9 +813,10 @@ describe("ChatSyncCoreWorker integration", () => { }) const outboundResult = await runEffect( - ChatSyncCoreWorker.syncHazelMessageCreateToAllConnections("discord", outboundMessageId).pipe( - Effect.provide(workerLayer), - ), + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.syncHazelMessageCreateToAllConnections("discord", outboundMessageId) + }).pipe(Effect.provide(workerLayer)), ) recordChatSyncDiagnostic({ suite: "chat-sync-core-worker.integration", @@ -825,24 +856,30 @@ describe("ChatSyncCoreWorker integration", () => { }) const webhookIgnored = await runEffect( - ChatSyncCoreWorker.ingestMessageCreate({ - syncConnectionId: guardedLinkConnection, - externalChannelId: "11111111111111114" as ExternalChannelId, - externalMessageId: "22222222222222224" as ExternalMessageId, - externalWebhookId: "99999999999999999" as ExternalWebhookId, - content: "from webhook", - dedupeKey: "ext:webhook-origin", + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.ingestMessageCreate({ + syncConnectionId: guardedLinkConnection, + externalChannelId: "11111111111111114" as ExternalChannelId, + externalMessageId: "22222222222222224" as ExternalMessageId, + externalWebhookId: "99999999999999999" as ExternalWebhookId, + content: "from webhook", + dedupeKey: "ext:webhook-origin", + }) }).pipe(Effect.provide(workerLayer)), ) expect(webhookIgnored.status).toBe("ignored_webhook_origin") const inactiveIgnored = await runEffect( - ChatSyncCoreWorker.ingestMessageCreate({ - syncConnectionId: inactiveConnection, - externalChannelId: "11111111111111113" as ExternalChannelId, - externalMessageId: "22222222222222223" as ExternalMessageId, - content: "ignored", - dedupeKey: "ext:inactive", + Effect.gen(function* () { + const w = yield* ChatSyncCoreWorker + return yield* w.ingestMessageCreate({ + syncConnectionId: inactiveConnection, + externalChannelId: "11111111111111113" as ExternalChannelId, + externalMessageId: "22222222222222223" as ExternalMessageId, + content: "ignored", + dedupeKey: "ext:inactive", + }) }).pipe(Effect.provide(workerLayer)), ) expect(inactiveIgnored.status).toBe("ignored_connection_inactive") diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index 31ee34c3c..8a2d8591d 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -151,664 +151,657 @@ export interface ChatSyncIngressThreadCreate { readonly dedupeKey?: string } -export class ChatSyncCoreWorker extends ServiceMap.Service()("ChatSyncCoreWorker", { - make: Effect.gen(function* () { - const db = yield* Database.Database - const connectionRepo = yield* ChatSyncConnectionRepo - const channelLinkRepo = yield* ChatSyncChannelLinkRepo - const messageLinkRepo = yield* ChatSyncMessageLinkRepo - const eventReceiptRepo = yield* ChatSyncEventReceiptRepo - const messageRepo = yield* MessageRepo - const outboxRepo = yield* MessageOutboxRepo - const messageReactionRepo = yield* MessageReactionRepo - const channelRepo = yield* ChannelRepo - const integrationConnectionRepo = yield* IntegrationConnectionRepo - const userRepo = yield* UserRepo - const organizationMemberRepo = yield* OrganizationMemberRepo - const integrationBotService = yield* IntegrationBotService - const channelAccessSyncService = yield* ChannelAccessSyncService - const providerRegistry = yield* ChatSyncProviderRegistry - const discordApiClient = yield* Discord.DiscordApiClient - const { DiscordSyncWorker } = yield* Effect.promise(() => import("./discord-sync-worker")) - const discordSyncWorker = yield* DiscordSyncWorker as typeof DiscordSyncWorkerType - - const payloadHash = (value: unknown): string => - createHash("sha256").update(JSON.stringify(value)).digest("hex") - - const claimReceipt = Effect.fn("discordSyncWorker.claimReceipt")(function* (params: { - syncConnectionId: SyncConnectionId - channelLinkId?: SyncChannelLinkId - source: "hazel" | "external" - dedupeKey: string - }) { - return yield* eventReceiptRepo.claimByDedupeKey({ - syncConnectionId: params.syncConnectionId, - channelLinkId: params.channelLinkId, - source: params.source, - dedupeKey: params.dedupeKey, - }) +export class ChatSyncCoreWorker extends ServiceMap.Service()("ChatSyncCoreWorker") {} + +/** @internal — exported for integration tests that provide their own deps */ +export const ChatSyncCoreWorkerMake = Effect.gen(function* () { + const db = yield* Database.Database + const connectionRepo = yield* ChatSyncConnectionRepo + const channelLinkRepo = yield* ChatSyncChannelLinkRepo + const messageLinkRepo = yield* ChatSyncMessageLinkRepo + const eventReceiptRepo = yield* ChatSyncEventReceiptRepo + const messageRepo = yield* MessageRepo + const outboxRepo = yield* MessageOutboxRepo + const messageReactionRepo = yield* MessageReactionRepo + const channelRepo = yield* ChannelRepo + const integrationConnectionRepo = yield* IntegrationConnectionRepo + const userRepo = yield* UserRepo + const organizationMemberRepo = yield* OrganizationMemberRepo + const integrationBotService = yield* IntegrationBotService + const channelAccessSyncService = yield* ChannelAccessSyncService + const providerRegistry = yield* ChatSyncProviderRegistry + const discordApiClient = yield* Discord.DiscordApiClient + const { DiscordSyncWorker } = yield* Effect.promise(() => import("./discord-sync-worker")) + const discordSyncWorker = yield* DiscordSyncWorker as typeof DiscordSyncWorkerType + + const payloadHash = (value: unknown): string => + createHash("sha256").update(JSON.stringify(value)).digest("hex") + + const claimReceipt = Effect.fn("discordSyncWorker.claimReceipt")(function* (params: { + syncConnectionId: SyncConnectionId + channelLinkId?: SyncChannelLinkId + source: "hazel" | "external" + dedupeKey: string + }) { + return yield* eventReceiptRepo.claimByDedupeKey({ + syncConnectionId: params.syncConnectionId, + channelLinkId: params.channelLinkId, + source: params.source, + dedupeKey: params.dedupeKey, }) - - const writeReceipt = Effect.fn("discordSyncWorker.writeReceipt")(function* (params: { - syncConnectionId: SyncConnectionId - channelLinkId?: SyncChannelLinkId - source: "hazel" | "external" - dedupeKey: string - status?: "processed" | "ignored" | "failed" - errorMessage?: string - payload?: unknown - }) { - yield* eventReceiptRepo.updateByDedupeKey({ - syncConnectionId: params.syncConnectionId, - source: params.source, - dedupeKey: params.dedupeKey, - channelLinkId: params.channelLinkId, - externalEventId: null, - payloadHash: params.payload ? payloadHash(params.payload) : null, - status: params.status ?? "processed", - errorMessage: params.errorMessage ?? null, - }) + }) + + const writeReceipt = Effect.fn("discordSyncWorker.writeReceipt")(function* (params: { + syncConnectionId: SyncConnectionId + channelLinkId?: SyncChannelLinkId + source: "hazel" | "external" + dedupeKey: string + status?: "processed" | "ignored" | "failed" + errorMessage?: string + payload?: unknown + }) { + yield* eventReceiptRepo.updateByDedupeKey({ + syncConnectionId: params.syncConnectionId, + source: params.source, + dedupeKey: params.dedupeKey, + channelLinkId: params.channelLinkId, + externalEventId: null, + payloadHash: params.payload ? payloadHash(params.payload) : null, + status: params.status ?? "processed", + errorMessage: params.errorMessage ?? null, }) + }) + + const getProviderAdapter = Effect.fn("ChatSyncCoreWorker.getProviderAdapter")(function* ( + provider: ChatSyncProvider, + ) { + return yield* providerRegistry.getAdapter(provider) + }) + + const buildAttachmentPublicUrl = (baseUrl: string, attachmentId: string): string => { + const normalizedBase = baseUrl.replace(/\/+$/, "") + return `${normalizedBase}/${attachmentId}` + } + + const getAttachmentPublicUrlBase = Effect.fn("discordSyncWorker.getAttachmentPublicUrlBase")( + function* () { + const configuredBaseUrl = yield* Config.string("S3_PUBLIC_URL").pipe(Config.option) + if (Option.isNone(configuredBaseUrl) || configuredBaseUrl.value.trim().length === 0) { + return yield* Effect.fail( + new DiscordSyncConfigurationError({ + message: "S3_PUBLIC_URL is required for syncing message attachments", + }), + ) + } + return configuredBaseUrl.value.trim() + }, + ) - const getProviderAdapter = Effect.fn("ChatSyncCoreWorker.getProviderAdapter")(function* ( - provider: ChatSyncProvider, - ) { - return yield* providerRegistry.getAdapter(provider) - }) + const listMessageAttachmentsForOutboundSync = Effect.fn( + "discordSyncWorker.listMessageAttachmentsForOutboundSync", + )(function* (hazelMessageId: MessageId) { + const rows = yield* db.execute((client) => + client + .select({ + id: schema.attachmentsTable.id, + fileName: schema.attachmentsTable.fileName, + fileSize: schema.attachmentsTable.fileSize, + }) + .from(schema.attachmentsTable) + .where( + and( + eq(schema.attachmentsTable.messageId, hazelMessageId), + eq(schema.attachmentsTable.status, "complete"), + isNull(schema.attachmentsTable.deletedAt), + ), + ) + .orderBy(asc(schema.attachmentsTable.uploadedAt), asc(schema.attachmentsTable.id)), + ) - const buildAttachmentPublicUrl = (baseUrl: string, attachmentId: string): string => { - const normalizedBase = baseUrl.replace(/\/+$/, "") - return `${normalizedBase}/${attachmentId}` + if (rows.length === 0) { + return [] } - const getAttachmentPublicUrlBase = Effect.fn("discordSyncWorker.getAttachmentPublicUrlBase")( - function* () { - const configuredBaseUrl = yield* Config.string("S3_PUBLIC_URL").pipe(Config.option) - if (Option.isNone(configuredBaseUrl) || configuredBaseUrl.value.trim().length === 0) { - return yield* Effect.fail( - new DiscordSyncConfigurationError({ - message: "S3_PUBLIC_URL is required for syncing message attachments", - }), - ) - } - return configuredBaseUrl.value.trim() - }, - ) - - const listMessageAttachmentsForOutboundSync = Effect.fn( - "discordSyncWorker.listMessageAttachmentsForOutboundSync", - )(function* (hazelMessageId: MessageId) { - const rows = yield* db.execute((client) => - client - .select({ - id: schema.attachmentsTable.id, - fileName: schema.attachmentsTable.fileName, - fileSize: schema.attachmentsTable.fileSize, - }) - .from(schema.attachmentsTable) - .where( - and( - eq(schema.attachmentsTable.messageId, hazelMessageId), - eq(schema.attachmentsTable.status, "complete"), - isNull(schema.attachmentsTable.deletedAt), - ), - ) - .orderBy(asc(schema.attachmentsTable.uploadedAt), asc(schema.attachmentsTable.id)), + const publicUrlBase = yield* getAttachmentPublicUrlBase() + return rows.map((row) => ({ + id: row.id, + fileName: row.fileName, + fileSize: row.fileSize, + publicUrl: buildAttachmentPublicUrl(publicUrlBase, row.id), + })) + }) + + const getOrCreateShadowUserId = Effect.fn("discordSyncWorker.getOrCreateShadowUserId")( + function* (params: { + provider: ChatSyncProvider + organizationId: OrganizationId + externalUserId: ExternalUserId + displayName: string + avatarUrl: string | null + syncAvatarUrl?: boolean + }) { + const externalId = `${params.provider}-user-${params.externalUserId}` + const user = yield* userRepo.upsertByExternalId( + { + externalId, + email: `${externalId}@${params.provider}.internal`, + firstName: params.displayName, + lastName: "", + avatarUrl: params.avatarUrl ?? "", + userType: "machine", + settings: null, + isOnboarded: true, + timezone: null, + deletedAt: null, + }, + { syncAvatarUrl: params.syncAvatarUrl ?? false }, ) - if (rows.length === 0) { - return [] - } - - const publicUrlBase = yield* getAttachmentPublicUrlBase() - return rows.map((row) => ({ - id: row.id, - fileName: row.fileName, - fileSize: row.fileSize, - publicUrl: buildAttachmentPublicUrl(publicUrlBase, row.id), - })) - }) - - const getOrCreateShadowUserId = Effect.fn("discordSyncWorker.getOrCreateShadowUserId")( - function* (params: { - provider: ChatSyncProvider - organizationId: OrganizationId - externalUserId: ExternalUserId - displayName: string - avatarUrl: string | null - syncAvatarUrl?: boolean - }) { - const externalId = `${params.provider}-user-${params.externalUserId}` - const user = yield* userRepo.upsertByExternalId( - { - externalId, - email: `${externalId}@${params.provider}.internal`, - firstName: params.displayName, - lastName: "", - avatarUrl: params.avatarUrl ?? "", - userType: "machine", - settings: null, - isOnboarded: true, - timezone: null, - deletedAt: null, - }, - { syncAvatarUrl: params.syncAvatarUrl ?? false }, - ) + yield* organizationMemberRepo.upsertByOrgAndUser({ + organizationId: params.organizationId, + userId: user.id, + role: "member", + nickname: null, + joinedAt: new Date(), + invitedBy: null, + deletedAt: null, + }) - yield* organizationMemberRepo.upsertByOrgAndUser({ - organizationId: params.organizationId, - userId: user.id, - role: "member", - nickname: null, - joinedAt: new Date(), - invitedBy: null, - deletedAt: null, - }) + return user.id + }, + ) - return user.id - }, - ) + const decodeProvider = Schema.decodeUnknownSync(IntegrationConnection.IntegrationProvider) - const decodeProvider = Schema.decodeUnknownSync(IntegrationConnection.IntegrationProvider) + type WebhookPermissionStatus = "unknown" | "allowed" | "denied" - type WebhookPermissionStatus = "unknown" | "allowed" | "denied" + type WebhookPermissionState = { + status: WebhookPermissionStatus + checkedAt: string + reason?: string + } - type WebhookPermissionState = { - status: WebhookPermissionStatus - checkedAt: string - reason?: string + const getRawWebhookPermissionState = ( + settings: Record | null | undefined, + ): WebhookPermissionState | undefined => { + const raw = settings?.webhookPermission + if (!raw || typeof raw !== "object") { + return undefined } - const getRawWebhookPermissionState = ( - settings: Record | null | undefined, - ): WebhookPermissionState | undefined => { - const raw = settings?.webhookPermission - if (!raw || typeof raw !== "object") { - return undefined - } - - const status = (raw as { status?: unknown }).status - if (status !== "allowed" && status !== "denied" && status !== "unknown") { - return undefined - } - - const checkedAt = (raw as { checkedAt?: unknown }).checkedAt - const reason = (raw as { reason?: unknown }).reason - - return { - status, - checkedAt: - typeof checkedAt === "string" && checkedAt.trim().length > 0 - ? checkedAt - : new Date().toISOString(), - reason: typeof reason === "string" && reason.trim().length > 0 ? reason : undefined, - } + const status = (raw as { status?: unknown }).status + if (status !== "allowed" && status !== "denied" && status !== "unknown") { + return undefined } - const makeWebhookPermissionState = (params: { - status: WebhookPermissionStatus - reason?: string - }): WebhookPermissionState => ({ - status: params.status, - checkedAt: new Date().toISOString(), - ...(params.reason ? { reason: params.reason } : {}), - }) - - const isDiscordApiError = (error: unknown): error is Discord.DiscordApiError => - typeof error === "object" && - error !== null && - (error as { _tag?: unknown })._tag === "DiscordApiError" + const checkedAt = (raw as { checkedAt?: unknown }).checkedAt + const reason = (raw as { reason?: unknown }).reason - const isWebhookStrategyEnabled = ( - outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, - ): boolean => outboundIdentity.strategy === "webhook" + return { + status, + checkedAt: + typeof checkedAt === "string" && checkedAt.trim().length > 0 + ? checkedAt + : new Date().toISOString(), + reason: typeof reason === "string" && reason.trim().length > 0 ? reason : undefined, + } + } + + const makeWebhookPermissionState = (params: { + status: WebhookPermissionStatus + reason?: string + }): WebhookPermissionState => ({ + status: params.status, + checkedAt: new Date().toISOString(), + ...(params.reason ? { reason: params.reason } : {}), + }) + + const isDiscordApiError = (error: unknown): error is Discord.DiscordApiError => + typeof error === "object" && + error !== null && + (error as { _tag?: unknown })._tag === "DiscordApiError" + + const isWebhookStrategyEnabled = ( + outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, + ): boolean => outboundIdentity.strategy === "webhook" + + const defaultOutboundIdentitySettings = (): ChatSyncChannelLink.OutboundIdentitySettings => ({ + enabled: false, + strategy: "webhook", + providers: {}, + }) + + const getRawOutboundIdentitySettings = ( + settings: Record | null | undefined, + ): Record | undefined => { + const raw = settings?.outboundIdentity + if (!raw || typeof raw !== "object") { + return undefined + } + return raw as Record + } + + const getOutboundIdentitySettings = ( + settings: Record | null | undefined, + ): ChatSyncChannelLink.OutboundIdentitySettings => { + const raw = getRawOutboundIdentitySettings(settings) + if (raw === undefined) { + return defaultOutboundIdentitySettings() + } + try { + return Schema.decodeUnknownSync(ChatSyncChannelLink.OutboundIdentitySettings)(raw) + } catch { + return defaultOutboundIdentitySettings() + } + } + + const getDiscordWebhookConfig = ( + outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, + ): Option.Option => { + const providerConfig = (outboundIdentity.providers as Record)["discord"] + if ( + typeof providerConfig !== "object" || + providerConfig === null || + !(providerConfig as { kind?: unknown }).kind + ) { + return Option.none() + } - const defaultOutboundIdentitySettings = (): ChatSyncChannelLink.OutboundIdentitySettings => ({ - enabled: false, - strategy: "webhook", - providers: {}, - }) + if ((providerConfig as { kind?: string }).kind !== "discord.webhook") { + return Option.none() + } - const getRawOutboundIdentitySettings = ( - settings: Record | null | undefined, - ): Record | undefined => { - const raw = settings?.outboundIdentity - if (!raw || typeof raw !== "object") { - return undefined - } - return raw as Record + try { + return Option.some( + Schema.decodeUnknownSync(ChatSyncChannelLink.DiscordWebhookOutboundIdentityConfig)( + providerConfig, + ), + ) + } catch { + return Option.none() + } + } + + const shouldIgnoreWebhookOrigin = ( + provider: ChatSyncProvider, + settings: Record | null | undefined, + externalWebhookId: ExternalWebhookId | undefined, + ): boolean => { + if (!externalWebhookId || provider !== "discord") { + return false + } + const outboundIdentity = getOutboundIdentitySettings(settings) + if (!isWebhookStrategyEnabled(outboundIdentity)) { + return false } + const webhookConfig = getDiscordWebhookConfig(outboundIdentity) + return Option.isSome(webhookConfig) && webhookConfig.value.webhookId === externalWebhookId + } + + const persistWebhookIdentity = Effect.fn("discordSyncWorker.persistWebhookIdentity")(function* ( + link: { + id: SyncChannelLinkId + settings: Record | null + }, + outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, + webhookPermissionState?: WebhookPermissionState, + ) { + const currentRawOutboundIdentity = + getRawOutboundIdentitySettings(link.settings) ?? defaultOutboundIdentitySettings() + const currentProviders = + typeof currentRawOutboundIdentity.providers === "object" && + currentRawOutboundIdentity.providers !== null + ? (currentRawOutboundIdentity.providers as Record) + : {} + + const nextSettings = { + ...(link.settings ?? {}), + outboundIdentity: { + ...currentRawOutboundIdentity, + enabled: outboundIdentity.enabled, + strategy: outboundIdentity.strategy, + providers: { + ...currentProviders, + ...outboundIdentity.providers, + }, + }, + ...(webhookPermissionState ? { webhookPermission: webhookPermissionState } : {}), + } + yield* channelLinkRepo.updateSettings(link.id, nextSettings) + }) - const getOutboundIdentitySettings = ( - settings: Record | null | undefined, - ): ChatSyncChannelLink.OutboundIdentitySettings => { - const raw = getRawOutboundIdentitySettings(settings) - if (raw === undefined) { - return defaultOutboundIdentitySettings() + const ensureDiscordWebhookIdentity = Effect.fn("discordSyncWorker.ensureDiscordWebhookIdentity")( + function* (params: { + provider: ChatSyncProvider + link: { + id: SyncChannelLinkId + externalChannelId: ExternalChannelId + settings: Record | null } - try { - return Schema.decodeUnknownSync(ChatSyncChannelLink.OutboundIdentitySettings)(raw) - } catch { - return defaultOutboundIdentitySettings() + }) { + const outboundIdentity = getOutboundIdentitySettings(params.link.settings) + if (params.provider !== "discord") { + return Option.none() } - } - const getDiscordWebhookConfig = ( - outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, - ): Option.Option => { - const providerConfig = (outboundIdentity.providers as Record)["discord"] - if ( - typeof providerConfig !== "object" || - providerConfig === null || - !(providerConfig as { kind?: unknown }).kind - ) { + if (!isWebhookStrategyEnabled(outboundIdentity)) { return Option.none() } - if ((providerConfig as { kind?: string }).kind !== "discord.webhook") { + const webhookPermissionState = + getRawWebhookPermissionState(params.link.settings) ?? + makeWebhookPermissionState({ status: "unknown" }) + if (webhookPermissionState.status === "denied") { return Option.none() } - try { - return Option.some( - Schema.decodeUnknownSync(ChatSyncChannelLink.DiscordWebhookOutboundIdentityConfig)( - providerConfig, - ), + return yield* Effect.gen(function* () { + const currentConfig = getDiscordWebhookConfig(outboundIdentity) + if (Option.isSome(currentConfig)) { + if (!outboundIdentity.enabled) { + return Option.none() + } + return currentConfig + } + + const botTokenOption = yield* Config.redacted("DISCORD_BOT_TOKEN").pipe(Config.option) + if (Option.isNone(botTokenOption)) { + return Option.none() + } + const botToken = Redacted.value(botTokenOption.value) + + const created = yield* discordApiClient.createWebhook({ + channelId: params.link.externalChannelId, + botToken, + }) + + const nextConfig: ChatSyncChannelLink.DiscordWebhookOutboundIdentityConfig = { + kind: "discord.webhook", + webhookId: created.webhookId, + webhookToken: created.webhookToken, + } + + const nextOutboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings = { + enabled: true, + strategy: "webhook", + providers: { + ...outboundIdentity.providers, + discord: nextConfig, + }, + } + + yield* persistWebhookIdentity( + params.link, + nextOutboundIdentity, + makeWebhookPermissionState({ status: "allowed" }), ) - } catch { - return Option.none() - } - } + return Option.some(nextConfig) + }).pipe( + Effect.catch((error) => + Effect.gen(function* () { + if (isDiscordApiError(error) && error.status === 403) { + const fallbackOutboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings = { + enabled: outboundIdentity.enabled, + strategy: "fallback_bot", + providers: outboundIdentity.providers, + } - const shouldIgnoreWebhookOrigin = ( - provider: ChatSyncProvider, - settings: Record | null | undefined, - externalWebhookId: ExternalWebhookId | undefined, - ): boolean => { - if (!externalWebhookId || provider !== "discord") { - return false - } - const outboundIdentity = getOutboundIdentitySettings(settings) - if (!isWebhookStrategyEnabled(outboundIdentity)) { - return false + yield* persistWebhookIdentity( + params.link, + fallbackOutboundIdentity, + makeWebhookPermissionState({ + status: "denied", + reason: `Webhook create forbidden (HTTP ${error.status})`, + }), + ) + } else { + const reason = + typeof error === "object" && error !== null && "message" in error + ? String((error as { message?: unknown }).message) + : undefined + + yield* persistWebhookIdentity( + params.link, + outboundIdentity, + makeWebhookPermissionState({ status: "unknown", reason }), + ) + } + + yield* Effect.logWarning("Failed to provision Discord webhook identity", { + provider: params.provider, + externalChannelId: params.link.externalChannelId, + error: String(error), + }) + return Option.none() + }), + ), + ) + }, + ) + + const getDiscordWebhookIdentityMessageMetadata = Effect.fn( + "discordSyncWorker.getDiscordWebhookIdentityMessageMetadata", + )(function* (authorId: UserId) { + const userOption = yield* userRepo.findById(authorId) + if (Option.isNone(userOption)) { + return { + username: "Discord User", + avatarUrl: undefined as string | undefined, } - const webhookConfig = getDiscordWebhookConfig(outboundIdentity) - return Option.isSome(webhookConfig) && webhookConfig.value.webhookId === externalWebhookId } - const persistWebhookIdentity = Effect.fn("discordSyncWorker.persistWebhookIdentity")(function* ( + const user = userOption.value + const username = [user.firstName, user.lastName].filter(Boolean).join(" ").trim() || user.firstName + const avatarUrl = user.avatarUrl && user.avatarUrl.trim() ? user.avatarUrl : undefined + return { username, avatarUrl } + }) + + const sendDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.sendDiscordMessageViaWebhook")( + function* (params: { link: { id: SyncChannelLinkId + externalChannelId: ExternalChannelId settings: Record | null - }, - outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, - webhookPermissionState?: WebhookPermissionState, - ) { - const currentRawOutboundIdentity = - getRawOutboundIdentitySettings(link.settings) ?? defaultOutboundIdentitySettings() - const currentProviders = - typeof currentRawOutboundIdentity.providers === "object" && - currentRawOutboundIdentity.providers !== null - ? (currentRawOutboundIdentity.providers as Record) - : {} - - const nextSettings = { - ...(link.settings ?? {}), - outboundIdentity: { - ...currentRawOutboundIdentity, - enabled: outboundIdentity.enabled, - strategy: outboundIdentity.strategy, - providers: { - ...currentProviders, - ...outboundIdentity.providers, - }, - }, - ...(webhookPermissionState ? { webhookPermission: webhookPermissionState } : {}), } - yield* channelLinkRepo.updateSettings(link.id, nextSettings) - }) - - const ensureDiscordWebhookIdentity = Effect.fn("discordSyncWorker.ensureDiscordWebhookIdentity")( - function* (params: { - provider: ChatSyncProvider - link: { - id: SyncChannelLinkId - externalChannelId: ExternalChannelId - settings: Record | null - } - }) { + message: { authorId: UserId } + content: string + attachments?: ReadonlyArray + replyToExternalMessageId?: ExternalMessageId + }) { + return yield* Effect.gen(function* () { const outboundIdentity = getOutboundIdentitySettings(params.link.settings) - if (params.provider !== "discord") { - return Option.none() - } - if (!isWebhookStrategyEnabled(outboundIdentity)) { return Option.none() } - const webhookPermissionState = - getRawWebhookPermissionState(params.link.settings) ?? - makeWebhookPermissionState({ status: "unknown" }) - if (webhookPermissionState.status === "denied") { + const config = yield* ensureDiscordWebhookIdentity({ + provider: "discord", + link: params.link, + }) + if (Option.isNone(config)) { return Option.none() } - return yield* Effect.gen(function* () { - const currentConfig = getDiscordWebhookConfig(outboundIdentity) - if (Option.isSome(currentConfig)) { - if (!outboundIdentity.enabled) { - return Option.none() - } - return currentConfig - } + const metadata = yield* getDiscordWebhookIdentityMessageMetadata(params.message.authorId) + const outboundContent = + params.attachments && params.attachments.length > 0 + ? formatMessageContentWithAttachments({ + content: params.content, + attachments: params.attachments, + }) + : params.content + const outboundMessageId = yield* discordApiClient.executeWebhookMessage({ + webhookId: config.value.webhookId, + webhookToken: config.value.webhookToken, + content: outboundContent, + replyToExternalMessageId: params.replyToExternalMessageId, + username: metadata.username, + avatarUrl: metadata.avatarUrl ?? config.value.defaultAvatarUrl, + }) - const botTokenOption = yield* Config.redacted("DISCORD_BOT_TOKEN").pipe(Config.option) - if (Option.isNone(botTokenOption)) { + return Option.some(outboundMessageId as ExternalMessageId) + }).pipe( + Effect.catch((error) => + Effect.gen(function* () { + yield* Effect.logWarning("Discord webhook send failed; falling back to bot API", { + error: String(error), + }) return Option.none() - } - const botToken = Redacted.value(botTokenOption.value) - - const created = yield* discordApiClient.createWebhook({ - channelId: params.link.externalChannelId, - botToken, - }) + }), + ), + ) + }, + ) - const nextConfig: ChatSyncChannelLink.DiscordWebhookOutboundIdentityConfig = { - kind: "discord.webhook", - webhookId: created.webhookId, - webhookToken: created.webhookToken, - } + const updateDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.updateDiscordMessageViaWebhook")( + function* (params: { + link: { + id: SyncChannelLinkId + externalChannelId: ExternalChannelId + settings: Record | null + } + externalMessageId: ExternalMessageId + content: string + }) { + return yield* Effect.gen(function* () { + const outboundIdentity = getOutboundIdentitySettings(params.link.settings) + if (!isWebhookStrategyEnabled(outboundIdentity)) { + return false + } - const nextOutboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings = { - enabled: true, - strategy: "webhook", - providers: { - ...outboundIdentity.providers, - discord: nextConfig, - }, - } + const config = yield* ensureDiscordWebhookIdentity({ + provider: "discord", + link: params.link, + }) + if (Option.isNone(config)) { + return false + } - yield* persistWebhookIdentity( - params.link, - nextOutboundIdentity, - makeWebhookPermissionState({ status: "allowed" }), - ) - return Option.some(nextConfig) - }).pipe( - Effect.catch((error) => - Effect.gen(function* () { - if (isDiscordApiError(error) && error.status === 403) { - const fallbackOutboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings = - { - enabled: outboundIdentity.enabled, - strategy: "fallback_bot", - providers: outboundIdentity.providers, - } - - yield* persistWebhookIdentity( - params.link, - fallbackOutboundIdentity, - makeWebhookPermissionState({ - status: "denied", - reason: `Webhook create forbidden (HTTP ${error.status})`, - }), - ) - } else { - const reason = - typeof error === "object" && error !== null && "message" in error - ? String((error as { message?: unknown }).message) - : undefined - - yield* persistWebhookIdentity( - params.link, - outboundIdentity, - makeWebhookPermissionState({ status: "unknown", reason }), - ) - } + yield* discordApiClient.updateWebhookMessage({ + webhookId: config.value.webhookId, + webhookToken: config.value.webhookToken, + webhookMessageId: params.externalMessageId, + content: params.content, + }) - yield* Effect.logWarning("Failed to provision Discord webhook identity", { - provider: params.provider, - externalChannelId: params.link.externalChannelId, - error: String(error), - }) - return Option.none() - }), - ), - ) - }, - ) + return true + }).pipe( + Effect.catch((error) => + Effect.gen(function* () { + yield* Effect.logWarning("Discord webhook update failed; falling back to bot API", { + error: String(error), + }) + return false + }), + ), + ) + }, + ) - const getDiscordWebhookIdentityMessageMetadata = Effect.fn( - "discordSyncWorker.getDiscordWebhookIdentityMessageMetadata", - )(function* (authorId: UserId) { - const userOption = yield* userRepo.findById(authorId) - if (Option.isNone(userOption)) { - return { - username: "Discord User", - avatarUrl: undefined as string | undefined, - } + const deleteDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.deleteDiscordMessageViaWebhook")( + function* (params: { + link: { + id: SyncChannelLinkId + externalChannelId: ExternalChannelId + settings: Record | null } + externalMessageId: ExternalMessageId + }) { + return yield* Effect.gen(function* () { + const outboundIdentity = getOutboundIdentitySettings(params.link.settings) + if (!isWebhookStrategyEnabled(outboundIdentity)) { + return false + } - const user = userOption.value - const username = - [user.firstName, user.lastName].filter(Boolean).join(" ").trim() || user.firstName - const avatarUrl = user.avatarUrl && user.avatarUrl.trim() ? user.avatarUrl : undefined - return { username, avatarUrl } - }) - - const sendDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.sendDiscordMessageViaWebhook")( - function* (params: { - link: { - id: SyncChannelLinkId - externalChannelId: ExternalChannelId - settings: Record | null + const config = yield* ensureDiscordWebhookIdentity({ + provider: "discord", + link: params.link, + }) + if (Option.isNone(config)) { + return false } - message: { authorId: UserId } - content: string - attachments?: ReadonlyArray - replyToExternalMessageId?: ExternalMessageId - }) { - return yield* Effect.gen(function* () { - const outboundIdentity = getOutboundIdentitySettings(params.link.settings) - if (!isWebhookStrategyEnabled(outboundIdentity)) { - return Option.none() - } - const config = yield* ensureDiscordWebhookIdentity({ - provider: "discord", - link: params.link, - }) - if (Option.isNone(config)) { - return Option.none() - } + yield* discordApiClient.deleteWebhookMessage({ + webhookId: config.value.webhookId, + webhookToken: config.value.webhookToken, + webhookMessageId: params.externalMessageId, + }) - const metadata = yield* getDiscordWebhookIdentityMessageMetadata(params.message.authorId) - const outboundContent = - params.attachments && params.attachments.length > 0 - ? formatMessageContentWithAttachments({ - content: params.content, - attachments: params.attachments, - }) - : params.content - const outboundMessageId = yield* discordApiClient.executeWebhookMessage({ - webhookId: config.value.webhookId, - webhookToken: config.value.webhookToken, - content: outboundContent, - replyToExternalMessageId: params.replyToExternalMessageId, - username: metadata.username, - avatarUrl: metadata.avatarUrl ?? config.value.defaultAvatarUrl, - }) + return true + }).pipe( + Effect.catch((error) => + Effect.gen(function* () { + yield* Effect.logWarning("Discord webhook delete failed; falling back to bot API", { + error: String(error), + }) + return false + }), + ), + ) + }, + ) - return Option.some(outboundMessageId as ExternalMessageId) - }).pipe( - Effect.catch((error) => - Effect.gen(function* () { - yield* Effect.logWarning("Discord webhook send failed; falling back to bot API", { - error: String(error), - }) - return Option.none() - }), - ), - ) - }, + const resolveAuthorUserId = Effect.fn("discordSyncWorker.resolveAuthorUserId")(function* (params: { + provider: ChatSyncProvider + organizationId: OrganizationId + externalUserId: ExternalUserId + displayName: string + avatarUrl: string | null + syncAvatarUrl?: boolean + }) { + const linkedConnection = yield* integrationConnectionRepo.findActiveUserByExternalAccountId( + params.organizationId, + decodeProvider(params.provider), + params.externalUserId, ) - const updateDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.updateDiscordMessageViaWebhook")( - function* (params: { - link: { - id: SyncChannelLinkId - externalChannelId: ExternalChannelId - settings: Record | null - } - externalMessageId: ExternalMessageId - content: string - }) { - return yield* Effect.gen(function* () { - const outboundIdentity = getOutboundIdentitySettings(params.link.settings) - if (!isWebhookStrategyEnabled(outboundIdentity)) { - return false - } - - const config = yield* ensureDiscordWebhookIdentity({ - provider: "discord", - link: params.link, - }) - if (Option.isNone(config)) { - return false - } + if (Option.isSome(linkedConnection) && linkedConnection.value.userId) { + return linkedConnection.value.userId + } - yield* discordApiClient.updateWebhookMessage({ - webhookId: config.value.webhookId, - webhookToken: config.value.webhookToken, - webhookMessageId: params.externalMessageId, - content: params.content, - }) + const shouldSyncAvatarUrl = + params.syncAvatarUrl !== undefined + ? params.syncAvatarUrl + : !!(params.avatarUrl && params.avatarUrl.trim()) + + return yield* getOrCreateShadowUserId({ + provider: params.provider, + organizationId: params.organizationId, + externalUserId: params.externalUserId, + displayName: params.displayName, + avatarUrl: params.avatarUrl, + syncAvatarUrl: shouldSyncAvatarUrl, + }) + }) - return true - }).pipe( - Effect.catch((error) => - Effect.gen(function* () { - yield* Effect.logWarning( - "Discord webhook update failed; falling back to bot API", - { - error: String(error), - }, - ) - return false - }), - ), - ) - }, - ) + const DISCORD_USER_MENTION_PATTERN = /@\[userId:([^\]]+)\]/g - const deleteDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.deleteDiscordMessageViaWebhook")( - function* (params: { - link: { - id: SyncChannelLinkId - externalChannelId: ExternalChannelId - settings: Record | null - } - externalMessageId: ExternalMessageId - }) { - return yield* Effect.gen(function* () { - const outboundIdentity = getOutboundIdentitySettings(params.link.settings) - if (!isWebhookStrategyEnabled(outboundIdentity)) { - return false - } + const getDiscordMentionFallbackDisplayName = Effect.fn( + "discordSyncWorker.getDiscordMentionFallbackDisplayName", + )(function* (userId: UserId) { + const userOption = yield* userRepo.findById(userId) + if (Option.isNone(userOption)) { + return "@unknown-user" + } - const config = yield* ensureDiscordWebhookIdentity({ - provider: "discord", - link: params.link, - }) - if (Option.isNone(config)) { - return false - } + const user = userOption.value + const fullName = [user.firstName, user.lastName].filter(Boolean).join(" ").trim() + if (fullName.length > 0) { + return `@${fullName}` + } - yield* discordApiClient.deleteWebhookMessage({ - webhookId: config.value.webhookId, - webhookToken: config.value.webhookToken, - webhookMessageId: params.externalMessageId, - }) + const firstName = user.firstName.trim() + if (firstName.length > 0) { + return `@${firstName}` + } - return true - }).pipe( - Effect.catch((error) => - Effect.gen(function* () { - yield* Effect.logWarning( - "Discord webhook delete failed; falling back to bot API", - { - error: String(error), - }, - ) - return false - }), - ), - ) - }, - ) + return "@unknown-user" + }) - const resolveAuthorUserId = Effect.fn("discordSyncWorker.resolveAuthorUserId")(function* (params: { - provider: ChatSyncProvider - organizationId: OrganizationId - externalUserId: ExternalUserId - displayName: string - avatarUrl: string | null - syncAvatarUrl?: boolean - }) { - const linkedConnection = yield* integrationConnectionRepo.findActiveUserByExternalAccountId( - params.organizationId, - decodeProvider(params.provider), - params.externalUserId, - ) - - if (Option.isSome(linkedConnection) && linkedConnection.value.userId) { - return linkedConnection.value.userId - } - - const shouldSyncAvatarUrl = - params.syncAvatarUrl !== undefined - ? params.syncAvatarUrl - : !!(params.avatarUrl && params.avatarUrl.trim()) - - return yield* getOrCreateShadowUserId({ - provider: params.provider, - organizationId: params.organizationId, - externalUserId: params.externalUserId, - displayName: params.displayName, - avatarUrl: params.avatarUrl, - syncAvatarUrl: shouldSyncAvatarUrl, - }) - }) - - const DISCORD_USER_MENTION_PATTERN = /@\[userId:([^\]]+)\]/g - - const getDiscordMentionFallbackDisplayName = Effect.fn( - "discordSyncWorker.getDiscordMentionFallbackDisplayName", - )(function* (userId: UserId) { - const userOption = yield* userRepo.findById(userId) - if (Option.isNone(userOption)) { - return "@unknown-user" - } - - const user = userOption.value - const fullName = [user.firstName, user.lastName].filter(Boolean).join(" ").trim() - if (fullName.length > 0) { - return `@${fullName}` - } - - const firstName = user.firstName.trim() - if (firstName.length > 0) { - return `@${firstName}` - } - - return "@unknown-user" - }) - - const translateHazelMentionsForDiscord = Effect.fn( - "discordSyncWorker.translateHazelMentionsForDiscord", - )(function* (params: { organizationId: OrganizationId; content: string }) { - const userIds = [...params.content.matchAll(DISCORD_USER_MENTION_PATTERN)].map( - (match) => match[1] as UserId, + const translateHazelMentionsForDiscord = Effect.fn("discordSyncWorker.translateHazelMentionsForDiscord")( + function* (params: { organizationId: OrganizationId; content: string }) { + const userIds = [...params.content.matchAll(DISCORD_USER_MENTION_PATTERN)].map( + (match) => match[1] as UserId, ) if (userIds.length === 0) { @@ -842,524 +835,506 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() DISCORD_USER_MENTION_PATTERN, (fullMatch, userId: string) => replacements.get(userId) ?? fullMatch, ) - }) + }, + ) - const normalizeChannelLinkExternalId = (link: T) => ({ - ...link, - externalChannelId: link.externalChannelId as ExternalChannelId, - }) + const normalizeChannelLinkExternalId = (link: T) => ({ + ...link, + externalChannelId: link.externalChannelId as ExternalChannelId, + }) - const normalizeMessageLinkExternalId = (messageLink: T) => ({ - ...messageLink, - externalMessageId: messageLink.externalMessageId as ExternalMessageId, - }) + const normalizeMessageLinkExternalId = (messageLink: T) => ({ + ...messageLink, + externalMessageId: messageLink.externalMessageId as ExternalMessageId, + }) - const resolveExternalMessageId = Effect.fn("discordSyncWorker.resolveExternalMessageId")( - function* (params: { - syncConnectionId: SyncConnectionId - hazelMessageId: MessageId - preferredChannelLinkId?: SyncChannelLinkId - }) { - if (params.preferredChannelLinkId) { - const preferred = yield* messageLinkRepo.findByHazelMessage( - params.preferredChannelLinkId, - params.hazelMessageId, - ) + const resolveExternalMessageId = Effect.fn("discordSyncWorker.resolveExternalMessageId")( + function* (params: { + syncConnectionId: SyncConnectionId + hazelMessageId: MessageId + preferredChannelLinkId?: SyncChannelLinkId + }) { + if (params.preferredChannelLinkId) { + const preferred = yield* messageLinkRepo.findByHazelMessage( + params.preferredChannelLinkId, + params.hazelMessageId, + ) - if (Option.isSome(preferred)) { - return Option.some(preferred.value.externalMessageId as ExternalMessageId) - } + if (Option.isSome(preferred)) { + return Option.some(preferred.value.externalMessageId as ExternalMessageId) } + } - const links = yield* db.execute((client) => - client - .select({ - externalMessageId: schema.chatSyncMessageLinksTable.externalMessageId, - }) - .from(schema.chatSyncMessageLinksTable) - .innerJoin( - schema.chatSyncChannelLinksTable, - and( - eq( - schema.chatSyncChannelLinksTable.id, - schema.chatSyncMessageLinksTable.channelLinkId, - ), - eq( - schema.chatSyncChannelLinksTable.syncConnectionId, - params.syncConnectionId, - ), - isNull(schema.chatSyncChannelLinksTable.deletedAt), - ), - ) - .where( - and( - eq(schema.chatSyncMessageLinksTable.hazelMessageId, params.hazelMessageId), - isNull(schema.chatSyncMessageLinksTable.deletedAt), + const links = yield* db.execute((client) => + client + .select({ + externalMessageId: schema.chatSyncMessageLinksTable.externalMessageId, + }) + .from(schema.chatSyncMessageLinksTable) + .innerJoin( + schema.chatSyncChannelLinksTable, + and( + eq( + schema.chatSyncChannelLinksTable.id, + schema.chatSyncMessageLinksTable.channelLinkId, ), - ) - .limit(1), + eq(schema.chatSyncChannelLinksTable.syncConnectionId, params.syncConnectionId), + isNull(schema.chatSyncChannelLinksTable.deletedAt), + ), + ) + .where( + and( + eq(schema.chatSyncMessageLinksTable.hazelMessageId, params.hazelMessageId), + isNull(schema.chatSyncMessageLinksTable.deletedAt), + ), + ) + .limit(1), + ) + return Option.fromNullishOr(links[0]?.externalMessageId as ExternalMessageId | undefined) + }, + ) + + const resolveHazelMessageId = Effect.fn("discordSyncWorker.resolveHazelMessageId")(function* (params: { + syncConnectionId: SyncConnectionId + externalMessageId: ExternalMessageId + preferredChannelLinkId?: SyncChannelLinkId + }) { + if (params.preferredChannelLinkId) { + const preferred = yield* messageLinkRepo.findByExternalMessage( + params.preferredChannelLinkId, + params.externalMessageId, + ) + + if (Option.isSome(preferred)) { + return Option.some(preferred.value.hazelMessageId) + } + } + + const links = yield* db.execute((client) => + client + .select({ + hazelMessageId: schema.chatSyncMessageLinksTable.hazelMessageId, + }) + .from(schema.chatSyncMessageLinksTable) + .innerJoin( + schema.chatSyncChannelLinksTable, + and( + eq( + schema.chatSyncChannelLinksTable.id, + schema.chatSyncMessageLinksTable.channelLinkId, + ), + eq(schema.chatSyncChannelLinksTable.syncConnectionId, params.syncConnectionId), + isNull(schema.chatSyncChannelLinksTable.deletedAt), + ), ) - return Option.fromNullishOr(links[0]?.externalMessageId as ExternalMessageId | undefined) - }, + .where( + and( + eq(schema.chatSyncMessageLinksTable.externalMessageId, params.externalMessageId), + isNull(schema.chatSyncMessageLinksTable.deletedAt), + ), + ) + .limit(1), + ) + return Option.fromNullishOr(links[0]?.hazelMessageId) + }) + + const resolveOrCreateOutboundLinkForMessage = Effect.fn( + "discordSyncWorker.resolveOrCreateOutboundLinkForMessage", + )(function* (params: { + syncConnectionId: SyncConnectionId + provider: ChatSyncProvider + hazelChannelId: ChannelId + }) { + const directLink = yield* channelLinkRepo.findByHazelChannel( + params.syncConnectionId, + params.hazelChannelId, ) - const resolveHazelMessageId = Effect.fn("discordSyncWorker.resolveHazelMessageId")( - function* (params: { - syncConnectionId: SyncConnectionId - externalMessageId: ExternalMessageId - preferredChannelLinkId?: SyncChannelLinkId - }) { - if (params.preferredChannelLinkId) { - const preferred = yield* messageLinkRepo.findByExternalMessage( - params.preferredChannelLinkId, - params.externalMessageId, - ) + if (Option.isSome(directLink)) { + return normalizeChannelLinkExternalId(directLink.value) + } - if (Option.isSome(preferred)) { - return Option.some(preferred.value.hazelMessageId) - } - } + const channelOption = yield* channelRepo.findById(params.hazelChannelId) + if (Option.isNone(channelOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: params.syncConnectionId, + }), + ) + } + const channel = channelOption.value - const links = yield* db.execute((client) => - client - .select({ - hazelMessageId: schema.chatSyncMessageLinksTable.hazelMessageId, - }) - .from(schema.chatSyncMessageLinksTable) - .innerJoin( - schema.chatSyncChannelLinksTable, - and( - eq( - schema.chatSyncChannelLinksTable.id, - schema.chatSyncMessageLinksTable.channelLinkId, - ), - eq( - schema.chatSyncChannelLinksTable.syncConnectionId, - params.syncConnectionId, - ), - isNull(schema.chatSyncChannelLinksTable.deletedAt), - ), - ) - .where( - and( - eq( - schema.chatSyncMessageLinksTable.externalMessageId, - params.externalMessageId, - ), - isNull(schema.chatSyncMessageLinksTable.deletedAt), - ), - ) - .limit(1), + if (channel.type !== "thread" || !channel.parentChannelId) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: params.syncConnectionId, + }), + ) + } + + const parentLinkOption = yield* channelLinkRepo.findByHazelChannel( + params.syncConnectionId, + channel.parentChannelId, + ) + + if (Option.isNone(parentLinkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: params.syncConnectionId, + }), + ) + } + const parentLink = parentLinkOption.value + + const rootMessages = yield* db.execute((client) => + client + .select({ + id: schema.messagesTable.id, + }) + .from(schema.messagesTable) + .where( + and( + eq(schema.messagesTable.threadChannelId, params.hazelChannelId), + isNull(schema.messagesTable.deletedAt), + ), ) - return Option.fromNullishOr(links[0]?.hazelMessageId) - }, + .orderBy(asc(schema.messagesTable.createdAt), asc(schema.messagesTable.id)) + .limit(1), ) + const rootMessageId = rootMessages[0]?.id + if (!rootMessageId) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: params.syncConnectionId, + }), + ) + } - const resolveOrCreateOutboundLinkForMessage = Effect.fn( - "discordSyncWorker.resolveOrCreateOutboundLinkForMessage", - )(function* (params: { - syncConnectionId: SyncConnectionId - provider: ChatSyncProvider - hazelChannelId: ChannelId - }) { - const directLink = yield* channelLinkRepo.findByHazelChannel( - params.syncConnectionId, - params.hazelChannelId, + const rootMessageLinkOption = yield* messageLinkRepo.findByHazelMessage(parentLink.id, rootMessageId) + + if (Option.isNone(rootMessageLinkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: params.syncConnectionId, + }), ) + } - if (Option.isSome(directLink)) { - return normalizeChannelLinkExternalId(directLink.value) - } + const adapter = yield* getProviderAdapter(params.provider) + const externalThreadId = yield* adapter.createThread({ + externalChannelId: normalizeChannelLinkExternalId(parentLink).externalChannelId, + externalMessageId: normalizeMessageLinkExternalId(rootMessageLinkOption.value).externalMessageId, + name: channel.name, + }) + const externalThreadChannelId = externalThreadId as unknown as ExternalChannelId - const channelOption = yield* channelRepo.findById(params.hazelChannelId) - if (Option.isNone(channelOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: params.syncConnectionId, - }), - ) - } - const channel = channelOption.value + const existingThreadLink = yield* channelLinkRepo.findByExternalChannel( + params.syncConnectionId, + externalThreadChannelId, + ) - if (channel.type !== "thread" || !channel.parentChannelId) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: params.syncConnectionId, - }), - ) - } + if (Option.isSome(existingThreadLink)) { + return normalizeChannelLinkExternalId(existingThreadLink.value) + } + + const [threadLink] = yield* channelLinkRepo.insert({ + syncConnectionId: params.syncConnectionId, + hazelChannelId: channel.id, + externalChannelId: externalThreadChannelId, + externalChannelName: channel.name, + direction: parentLink.direction, + isActive: true, + settings: parentLink.settings, + lastSyncedAt: null, + deletedAt: null, + }) + + yield* channelAccessSyncService.syncChannel(channel.id) + return normalizeChannelLinkExternalId(threadLink) + }) + + const syncHazelMessageToProvider = Effect.fn("discordSyncWorker.syncHazelMessageToProvider")(function* ( + syncConnectionId: SyncConnectionId, + hazelMessageId: MessageId, + dedupeKeyOverride?: string, + ) { + const dedupeKey = dedupeKeyOverride ?? `hazel:message:create:${hazelMessageId}` + const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) + if (!claimed) { + return { status: "deduped" as const } + } + + const connectionOption = yield* connectionRepo.findById(syncConnectionId) - const parentLinkOption = yield* channelLinkRepo.findByHazelChannel( - params.syncConnectionId, - channel.parentChannelId, + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId, + }), ) + } + const connection = connectionOption.value - if (Option.isNone(parentLinkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: params.syncConnectionId, - }), + const messageOption = yield* messageRepo.findById(hazelMessageId) + if (Option.isNone(messageOption)) { + return yield* Effect.fail( + new DiscordSyncMessageNotFoundError({ + messageId: hazelMessageId, + }), + ) + } + const message = messageOption.value + const adapter = yield* getProviderAdapter(connection.provider) + const link = yield* resolveOrCreateOutboundLinkForMessage({ + syncConnectionId, + provider: connection.provider, + hazelChannelId: message.channelId, + }) + const normalizedLink = normalizeChannelLinkExternalId(link) + + const existingMessageLink = yield* messageLinkRepo.findByHazelMessage(link.id, hazelMessageId) + + if (Option.isSome(existingMessageLink)) { + yield* writeReceipt({ + syncConnectionId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, + status: "ignored", + }) + return { status: "already_linked" as const } + } + + const replyToExternalMessageId = message.replyToMessageId + ? yield* resolveExternalMessageId({ + syncConnectionId, + hazelMessageId: message.replyToMessageId, + preferredChannelLinkId: link.id, + }).pipe( + Effect.map((id) => + Option.match(id, { + onNone: () => undefined, + onSome: (value) => value as ExternalMessageId, + }), + ), ) + : undefined + const attachments = yield* listMessageAttachmentsForOutboundSync(message.id) + const outboundContent = + connection.provider === "discord" + ? yield* translateHazelMentionsForDiscord({ + organizationId: connection.organizationId, + content: message.content, + }) + : message.content + + let externalMessageId: ExternalMessageId + if (connection.provider === "discord") { + const webhookMessageId = yield* sendDiscordMessageViaWebhook({ + link: { + id: normalizedLink.id, + externalChannelId: normalizedLink.externalChannelId, + settings: link.settings, + }, + message: { + authorId: message.authorId, + }, + content: outboundContent, + attachments, + replyToExternalMessageId, + }) + + if (Option.isSome(webhookMessageId)) { + externalMessageId = webhookMessageId.value + } else if (attachments.length > 0) { + externalMessageId = yield* adapter.createMessageWithAttachments({ + externalChannelId: normalizedLink.externalChannelId, + content: outboundContent, + attachments, + replyToExternalMessageId, + }) + } else { + externalMessageId = yield* adapter.createMessage({ + externalChannelId: normalizedLink.externalChannelId, + content: outboundContent, + replyToExternalMessageId, + }) } - const parentLink = parentLinkOption.value + } else if (attachments.length > 0) { + externalMessageId = yield* adapter.createMessageWithAttachments({ + externalChannelId: normalizedLink.externalChannelId, + content: message.content, + attachments, + replyToExternalMessageId, + }) + } else { + externalMessageId = yield* adapter.createMessage({ + externalChannelId: normalizedLink.externalChannelId, + content: message.content, + replyToExternalMessageId, + }) + } + + yield* messageLinkRepo.insert({ + channelLinkId: link.id, + hazelMessageId: message.id, + externalMessageId, + source: "hazel", + rootHazelMessageId: null, + rootExternalMessageId: null, + hazelThreadChannelId: message.threadChannelId, + externalThreadId: null, + deletedAt: null, + }) + + yield* writeReceipt({ + syncConnectionId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, + payload: { + hazelMessageId, + externalMessageId, + }, + }) + yield* connectionRepo.updateLastSyncedAt(syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) + + return { status: "synced" as const, externalMessageId } + }) + + const syncConnection = Effect.fn("discordSyncWorker.syncConnection")(function* ( + syncConnectionId: SyncConnectionId, + maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, + ) { + const connectionOption = yield* connectionRepo.findById(syncConnectionId) + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId, + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + return { sent: 0, skipped: 0, failed: 0 } + } + + const links = yield* channelLinkRepo.findActiveBySyncConnection(syncConnectionId) - const rootMessages = yield* db.execute((client) => + let sent = 0 + let skipped = 0 + let failed = 0 + + for (const link of links) { + const unsyncedMessages = yield* db.execute((client) => client .select({ id: schema.messagesTable.id, }) .from(schema.messagesTable) + .leftJoin( + schema.chatSyncMessageLinksTable, + and( + eq(schema.chatSyncMessageLinksTable.channelLinkId, link.id), + eq(schema.chatSyncMessageLinksTable.hazelMessageId, schema.messagesTable.id), + isNull(schema.chatSyncMessageLinksTable.deletedAt), + ), + ) .where( and( - eq(schema.messagesTable.threadChannelId, params.hazelChannelId), + eq(schema.messagesTable.channelId, link.hazelChannelId), isNull(schema.messagesTable.deletedAt), + isNull(schema.chatSyncMessageLinksTable.id), ), ) .orderBy(asc(schema.messagesTable.createdAt), asc(schema.messagesTable.id)) - .limit(1), + .limit(maxMessagesPerChannel), ) - const rootMessageId = rootMessages[0]?.id - if (!rootMessageId) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: params.syncConnectionId, - }), + + for (const unsyncedMessage of unsyncedMessages) { + const result = yield* syncHazelMessageToProvider(syncConnectionId, unsyncedMessage.id).pipe( + Effect.result, ) + if (result._tag === "Success") { + if (result.success.status === "synced") { + sent++ + } else { + skipped++ + } + } else { + failed++ + yield* Effect.logWarning("Failed to sync Hazel message to provider", { + provider: connection.provider, + syncConnectionId, + hazelMessageId: unsyncedMessage.id, + error: result.failure, + }) + } } + } - const rootMessageLinkOption = yield* messageLinkRepo.findByHazelMessage( - parentLink.id, - rootMessageId, - ) + return { sent, skipped, failed } + }) + + const syncHazelMessageUpdateToProvider = Effect.fn("discordSyncWorker.syncHazelMessageUpdateToProvider")( + function* ( + syncConnectionId: SyncConnectionId, + hazelMessageId: MessageId, + dedupeKeyOverride?: string, + ) { + const dedupeKey = dedupeKeyOverride ?? `hazel:message:update:${hazelMessageId}` + const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) + if (!claimed) { + return { status: "deduped" as const } + } - if (Option.isNone(rootMessageLinkOption)) { + const connectionOption = yield* connectionRepo.findById(syncConnectionId) + if (Option.isNone(connectionOption)) { return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: params.syncConnectionId, + new DiscordSyncConnectionNotFoundError({ + syncConnectionId, }), ) } + const connection = connectionOption.value + const adapter = yield* getProviderAdapter(connection.provider) - const adapter = yield* getProviderAdapter(params.provider) - const externalThreadId = yield* adapter.createThread({ - externalChannelId: normalizeChannelLinkExternalId(parentLink).externalChannelId, - externalMessageId: normalizeMessageLinkExternalId(rootMessageLinkOption.value) - .externalMessageId, - name: channel.name, - }) - const externalThreadChannelId = externalThreadId as unknown as ExternalChannelId - - const existingThreadLink = yield* channelLinkRepo.findByExternalChannel( - params.syncConnectionId, - externalThreadChannelId, - ) - - if (Option.isSome(existingThreadLink)) { - return normalizeChannelLinkExternalId(existingThreadLink.value) + const messageOption = yield* messageRepo.findById(hazelMessageId) + if (Option.isNone(messageOption)) { + return yield* Effect.fail(new DiscordSyncMessageNotFoundError({ messageId: hazelMessageId })) } + const message = messageOption.value - const [threadLink] = yield* channelLinkRepo.insert({ - syncConnectionId: params.syncConnectionId, - hazelChannelId: channel.id, - externalChannelId: externalThreadChannelId, - externalChannelName: channel.name, - direction: parentLink.direction, - isActive: true, - settings: parentLink.settings, - lastSyncedAt: null, - deletedAt: null, + const link = yield* resolveOrCreateOutboundLinkForMessage({ + syncConnectionId, + provider: connection.provider, + hazelChannelId: message.channelId, }) + const normalizedLink = normalizeChannelLinkExternalId(link) - yield* channelAccessSyncService.syncChannel(channel.id) - return normalizeChannelLinkExternalId(threadLink) - }) - - const syncHazelMessageToProvider = Effect.fn("discordSyncWorker.syncHazelMessageToProvider")( - function* ( - syncConnectionId: SyncConnectionId, - hazelMessageId: MessageId, - dedupeKeyOverride?: string, - ) { - const dedupeKey = dedupeKeyOverride ?? `hazel:message:create:${hazelMessageId}` - const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) - if (!claimed) { - return { status: "deduped" as const } - } - - const connectionOption = yield* connectionRepo.findById(syncConnectionId) - - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId, - }), - ) - } - const connection = connectionOption.value + const messageLinkOption = yield* messageLinkRepo.findByHazelMessage(link.id, hazelMessageId) - const messageOption = yield* messageRepo.findById(hazelMessageId) - if (Option.isNone(messageOption)) { - return yield* Effect.fail( - new DiscordSyncMessageNotFoundError({ - messageId: hazelMessageId, - }), - ) - } - const message = messageOption.value - const adapter = yield* getProviderAdapter(connection.provider) - const link = yield* resolveOrCreateOutboundLinkForMessage({ + if (Option.isNone(messageLinkOption)) { + yield* writeReceipt({ syncConnectionId, - provider: connection.provider, - hazelChannelId: message.channelId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, + status: "ignored", + payload: { hazelMessageId }, }) - const normalizedLink = normalizeChannelLinkExternalId(link) - - const existingMessageLink = yield* messageLinkRepo.findByHazelMessage(link.id, hazelMessageId) - - if (Option.isSome(existingMessageLink)) { - yield* writeReceipt({ - syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, - status: "ignored", - }) - return { status: "already_linked" as const } - } - - const replyToExternalMessageId = message.replyToMessageId - ? yield* resolveExternalMessageId({ - syncConnectionId, - hazelMessageId: message.replyToMessageId, - preferredChannelLinkId: link.id, - }).pipe( - Effect.map((id) => - Option.match(id, { - onNone: () => undefined, - onSome: (value) => value as ExternalMessageId, - }), - ), - ) - : undefined - const attachments = yield* listMessageAttachmentsForOutboundSync(message.id) - const outboundContent = - connection.provider === "discord" - ? yield* translateHazelMentionsForDiscord({ - organizationId: connection.organizationId, - content: message.content, - }) - : message.content - - let externalMessageId: ExternalMessageId - if (connection.provider === "discord") { - const webhookMessageId = yield* sendDiscordMessageViaWebhook({ - link: { - id: normalizedLink.id, - externalChannelId: normalizedLink.externalChannelId, - settings: link.settings, - }, - message: { - authorId: message.authorId, - }, - content: outboundContent, - attachments, - replyToExternalMessageId, - }) - - if (Option.isSome(webhookMessageId)) { - externalMessageId = webhookMessageId.value - } else if (attachments.length > 0) { - externalMessageId = yield* adapter.createMessageWithAttachments({ - externalChannelId: normalizedLink.externalChannelId, - content: outboundContent, - attachments, - replyToExternalMessageId, - }) - } else { - externalMessageId = yield* adapter.createMessage({ - externalChannelId: normalizedLink.externalChannelId, - content: outboundContent, - replyToExternalMessageId, - }) - } - } else if (attachments.length > 0) { - externalMessageId = yield* adapter.createMessageWithAttachments({ - externalChannelId: normalizedLink.externalChannelId, - content: message.content, - attachments, - replyToExternalMessageId, - }) - } else { - externalMessageId = yield* adapter.createMessage({ - externalChannelId: normalizedLink.externalChannelId, - content: message.content, - replyToExternalMessageId, - }) - } - - yield* messageLinkRepo.insert({ - channelLinkId: link.id, - hazelMessageId: message.id, - externalMessageId, - source: "hazel", - rootHazelMessageId: null, - rootExternalMessageId: null, - hazelThreadChannelId: message.threadChannelId, - externalThreadId: null, - deletedAt: null, - }) - - yield* writeReceipt({ - syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, - payload: { - hazelMessageId, - externalMessageId, - }, - }) - yield* connectionRepo.updateLastSyncedAt(syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) - - return { status: "synced" as const, externalMessageId } - }, - ) - - const syncConnection = Effect.fn("discordSyncWorker.syncConnection")(function* ( - syncConnectionId: SyncConnectionId, - maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, - ) { - const connectionOption = yield* connectionRepo.findById(syncConnectionId) - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId, - }), - ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - return { sent: 0, skipped: 0, failed: 0 } - } - - const links = yield* channelLinkRepo.findActiveBySyncConnection(syncConnectionId) - - let sent = 0 - let skipped = 0 - let failed = 0 - - for (const link of links) { - const unsyncedMessages = yield* db.execute((client) => - client - .select({ - id: schema.messagesTable.id, - }) - .from(schema.messagesTable) - .leftJoin( - schema.chatSyncMessageLinksTable, - and( - eq(schema.chatSyncMessageLinksTable.channelLinkId, link.id), - eq(schema.chatSyncMessageLinksTable.hazelMessageId, schema.messagesTable.id), - isNull(schema.chatSyncMessageLinksTable.deletedAt), - ), - ) - .where( - and( - eq(schema.messagesTable.channelId, link.hazelChannelId), - isNull(schema.messagesTable.deletedAt), - isNull(schema.chatSyncMessageLinksTable.id), - ), - ) - .orderBy(asc(schema.messagesTable.createdAt), asc(schema.messagesTable.id)) - .limit(maxMessagesPerChannel), - ) - - for (const unsyncedMessage of unsyncedMessages) { - const result = yield* syncHazelMessageToProvider( - syncConnectionId, - unsyncedMessage.id, - ).pipe(Effect.result) - if (result._tag === "Success") { - if (result.success.status === "synced") { - sent++ - } else { - skipped++ - } - } else { - failed++ - yield* Effect.logWarning("Failed to sync Hazel message to provider", { - provider: connection.provider, - syncConnectionId, - hazelMessageId: unsyncedMessage.id, - error: result.failure, - }) - } - } - } - - return { sent, skipped, failed } - }) - - const syncHazelMessageUpdateToProvider = Effect.fn( - "discordSyncWorker.syncHazelMessageUpdateToProvider", - )(function* ( - syncConnectionId: SyncConnectionId, - hazelMessageId: MessageId, - dedupeKeyOverride?: string, - ) { - const dedupeKey = dedupeKeyOverride ?? `hazel:message:update:${hazelMessageId}` - const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) - if (!claimed) { - return { status: "deduped" as const } - } - - const connectionOption = yield* connectionRepo.findById(syncConnectionId) - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId, - }), - ) - } - const connection = connectionOption.value - const adapter = yield* getProviderAdapter(connection.provider) - - const messageOption = yield* messageRepo.findById(hazelMessageId) - if (Option.isNone(messageOption)) { - return yield* Effect.fail(new DiscordSyncMessageNotFoundError({ messageId: hazelMessageId })) - } - const message = messageOption.value - - const link = yield* resolveOrCreateOutboundLinkForMessage({ - syncConnectionId, - provider: connection.provider, - hazelChannelId: message.channelId, - }) - const normalizedLink = normalizeChannelLinkExternalId(link) - - const messageLinkOption = yield* messageLinkRepo.findByHazelMessage(link.id, hazelMessageId) - - if (Option.isNone(messageLinkOption)) { - yield* writeReceipt({ - syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, - status: "ignored", - payload: { hazelMessageId }, - }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - const normalizedMessageLink = normalizeMessageLinkExternalId(messageLink) - const outboundContent = - connection.provider === "discord" - ? yield* translateHazelMentionsForDiscord({ - organizationId: connection.organizationId, - content: message.content, - }) - : message.content + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + const normalizedMessageLink = normalizeMessageLinkExternalId(messageLink) + const outboundContent = + connection.provider === "discord" + ? yield* translateHazelMentionsForDiscord({ + organizationId: connection.organizationId, + content: message.content, + }) + : message.content if (connection.provider !== "discord") { yield* adapter.updateMessage({ @@ -1405,11 +1380,11 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() status: "updated" as const, externalMessageId: normalizedMessageLink.externalMessageId, } - }) + }, + ) - const syncHazelMessageDeleteToProvider = Effect.fn( - "discordSyncWorker.syncHazelMessageDeleteToProvider", - )(function* ( + const syncHazelMessageDeleteToProvider = Effect.fn("discordSyncWorker.syncHazelMessageDeleteToProvider")( + function* ( syncConnectionId: SyncConnectionId, hazelMessageId: MessageId, dedupeKeyOverride?: string, @@ -1501,1287 +1476,1281 @@ export class ChatSyncCoreWorker extends ServiceMap.Service() status: "deleted" as const, externalMessageId: normalizedMessageLink.externalMessageId, } - }) - - const syncHazelReactionCreateToProvider = Effect.fn( - "discordSyncWorker.syncHazelReactionCreateToProvider", - )(function* ( - syncConnectionId: SyncConnectionId, - hazelReactionId: MessageReactionId, - dedupeKeyOverride?: string, - ) { - const dedupeKey = dedupeKeyOverride ?? `hazel:reaction:create:${hazelReactionId}` - const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) - if (!claimed) { - return { status: "deduped" as const } - } + }, + ) - const connectionOption = yield* connectionRepo.findById(syncConnectionId) - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId, - }), - ) - } - const connection = connectionOption.value - const adapter = yield* getProviderAdapter(connection.provider) + const syncHazelReactionCreateToProvider = Effect.fn( + "discordSyncWorker.syncHazelReactionCreateToProvider", + )(function* ( + syncConnectionId: SyncConnectionId, + hazelReactionId: MessageReactionId, + dedupeKeyOverride?: string, + ) { + const dedupeKey = dedupeKeyOverride ?? `hazel:reaction:create:${hazelReactionId}` + const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) + if (!claimed) { + return { status: "deduped" as const } + } - const reactionOption = yield* messageReactionRepo.findById(hazelReactionId) - if (Option.isNone(reactionOption)) { - yield* writeReceipt({ + const connectionOption = yield* connectionRepo.findById(syncConnectionId) + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ syncConnectionId, - source: "hazel", - dedupeKey, - status: "ignored", - }) - return { status: "ignored_missing_reaction" as const } - } - const reaction = reactionOption.value + }), + ) + } + const connection = connectionOption.value + const adapter = yield* getProviderAdapter(connection.provider) - const link = yield* resolveOrCreateOutboundLinkForMessage({ + const reactionOption = yield* messageReactionRepo.findById(hazelReactionId) + if (Option.isNone(reactionOption)) { + yield* writeReceipt({ syncConnectionId, - provider: connection.provider, - hazelChannelId: reaction.channelId, + source: "hazel", + dedupeKey, + status: "ignored", }) - const normalizedLink = normalizeChannelLinkExternalId(link) - - const messageLinkOption = yield* messageLinkRepo.findByHazelMessage(link.id, reaction.messageId) + return { status: "ignored_missing_reaction" as const } + } + const reaction = reactionOption.value - if (Option.isNone(messageLinkOption)) { - yield* writeReceipt({ - syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, - status: "ignored", - payload: { - hazelReactionId, - }, - }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - const normalizedMessageLink = normalizeMessageLinkExternalId(messageLink) + const link = yield* resolveOrCreateOutboundLinkForMessage({ + syncConnectionId, + provider: connection.provider, + hazelChannelId: reaction.channelId, + }) + const normalizedLink = normalizeChannelLinkExternalId(link) - yield* adapter.addReaction({ - externalChannelId: normalizedLink.externalChannelId, - externalMessageId: normalizedMessageLink.externalMessageId, - emoji: reaction.emoji, - }) + const messageLinkOption = yield* messageLinkRepo.findByHazelMessage(link.id, reaction.messageId) + if (Option.isNone(messageLinkOption)) { yield* writeReceipt({ syncConnectionId, channelLinkId: link.id, source: "hazel", dedupeKey, + status: "ignored", payload: { hazelReactionId, - externalMessageId: normalizedMessageLink.externalMessageId, - emoji: reaction.emoji, }, }) - yield* connectionRepo.updateLastSyncedAt(syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + const normalizedMessageLink = normalizeMessageLinkExternalId(messageLink) - return { - status: "created" as const, - externalMessageId: normalizedMessageLink.externalMessageId, - } + yield* adapter.addReaction({ + externalChannelId: normalizedLink.externalChannelId, + externalMessageId: normalizedMessageLink.externalMessageId, + emoji: reaction.emoji, }) - const syncHazelReactionDeleteToProvider = Effect.fn( - "discordSyncWorker.syncHazelReactionDeleteToProvider", - )(function* ( - syncConnectionId: SyncConnectionId, + yield* writeReceipt({ + syncConnectionId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, payload: { - hazelChannelId: ChannelId - hazelMessageId: MessageId - emoji: string - userId?: UserId + hazelReactionId, + externalMessageId: normalizedMessageLink.externalMessageId, + emoji: reaction.emoji, }, - dedupeKeyOverride?: string, - ) { - const dedupeKey = - dedupeKeyOverride ?? - `hazel:reaction:delete:${payload.hazelMessageId}:${payload.emoji}:${payload.userId ?? "unknown"}` - const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) - if (!claimed) { - return { status: "deduped" as const } - } + }) + yield* connectionRepo.updateLastSyncedAt(syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) - const connectionOption = yield* connectionRepo.findById(syncConnectionId) - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId, - }), - ) - } - const connection = connectionOption.value - const adapter = yield* getProviderAdapter(connection.provider) + return { + status: "created" as const, + externalMessageId: normalizedMessageLink.externalMessageId, + } + }) - const link = yield* resolveOrCreateOutboundLinkForMessage({ - syncConnectionId, - provider: connection.provider, - hazelChannelId: payload.hazelChannelId, - }) - const normalizedLink = normalizeChannelLinkExternalId(link) + const syncHazelReactionDeleteToProvider = Effect.fn( + "discordSyncWorker.syncHazelReactionDeleteToProvider", + )(function* ( + syncConnectionId: SyncConnectionId, + payload: { + hazelChannelId: ChannelId + hazelMessageId: MessageId + emoji: string + userId?: UserId + }, + dedupeKeyOverride?: string, + ) { + const dedupeKey = + dedupeKeyOverride ?? + `hazel:reaction:delete:${payload.hazelMessageId}:${payload.emoji}:${payload.userId ?? "unknown"}` + const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) + if (!claimed) { + return { status: "deduped" as const } + } - const messageLinkOption = yield* messageLinkRepo.findByHazelMessage( - link.id, - payload.hazelMessageId, + const connectionOption = yield* connectionRepo.findById(syncConnectionId) + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId, + }), ) + } + const connection = connectionOption.value + const adapter = yield* getProviderAdapter(connection.provider) - if (Option.isNone(messageLinkOption)) { - yield* writeReceipt({ - syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, - status: "ignored", - payload, - }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - const normalizedMessageLink = normalizeMessageLinkExternalId(messageLink) + const link = yield* resolveOrCreateOutboundLinkForMessage({ + syncConnectionId, + provider: connection.provider, + hazelChannelId: payload.hazelChannelId, + }) + const normalizedLink = normalizeChannelLinkExternalId(link) - const remainingReactions = yield* db.execute((client) => - client - .select({ - id: schema.messageReactionsTable.id, - }) - .from(schema.messageReactionsTable) - .where( - and( - eq(schema.messageReactionsTable.messageId, payload.hazelMessageId), - eq(schema.messageReactionsTable.emoji, payload.emoji), - ), - ) - .limit(1), - ) - if (remainingReactions.length > 0) { - yield* writeReceipt({ - syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, - status: "ignored", - payload: { - ...payload, - reason: "remaining_reactions_for_emoji", - }, - }) - return { status: "ignored_remaining_reactions" as const } - } + const messageLinkOption = yield* messageLinkRepo.findByHazelMessage(link.id, payload.hazelMessageId) - yield* adapter.removeReaction({ - externalChannelId: normalizedLink.externalChannelId, - externalMessageId: normalizedMessageLink.externalMessageId, - emoji: payload.emoji, + if (Option.isNone(messageLinkOption)) { + yield* writeReceipt({ + syncConnectionId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, + status: "ignored", + payload, }) + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + const normalizedMessageLink = normalizeMessageLinkExternalId(messageLink) + const remainingReactions = yield* db.execute((client) => + client + .select({ + id: schema.messageReactionsTable.id, + }) + .from(schema.messageReactionsTable) + .where( + and( + eq(schema.messageReactionsTable.messageId, payload.hazelMessageId), + eq(schema.messageReactionsTable.emoji, payload.emoji), + ), + ) + .limit(1), + ) + if (remainingReactions.length > 0) { yield* writeReceipt({ syncConnectionId, channelLinkId: link.id, source: "hazel", dedupeKey, + status: "ignored", payload: { ...payload, - externalMessageId: normalizedMessageLink.externalMessageId, + reason: "remaining_reactions_for_emoji", }, }) - yield* connectionRepo.updateLastSyncedAt(syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) + return { status: "ignored_remaining_reactions" as const } + } - return { - status: "deleted" as const, - externalMessageId: normalizedMessageLink.externalMessageId, - } + yield* adapter.removeReaction({ + externalChannelId: normalizedLink.externalChannelId, + externalMessageId: normalizedMessageLink.externalMessageId, + emoji: payload.emoji, }) - const getActiveOutboundTargets = Effect.fn("discordSyncWorker.getActiveOutboundTargets")(function* ( - hazelChannelId: ChannelId, - provider: ChatSyncProvider, - ) { - const targets = yield* db.execute((client) => - client - .select({ - syncConnectionId: schema.chatSyncConnectionsTable.id, - channelLinkId: schema.chatSyncChannelLinksTable.id, - direction: schema.chatSyncChannelLinksTable.direction, - }) - .from(schema.chatSyncChannelLinksTable) - .innerJoin( - schema.chatSyncConnectionsTable, - eq( - schema.chatSyncConnectionsTable.id, - schema.chatSyncChannelLinksTable.syncConnectionId, - ), - ) - .where( - and( - eq(schema.chatSyncChannelLinksTable.hazelChannelId, hazelChannelId), - eq(schema.chatSyncChannelLinksTable.isActive, true), - isNull(schema.chatSyncChannelLinksTable.deletedAt), - eq(schema.chatSyncConnectionsTable.provider, provider), - eq(schema.chatSyncConnectionsTable.status, "active"), - isNull(schema.chatSyncConnectionsTable.deletedAt), - ), - ), - ) - return targets + yield* writeReceipt({ + syncConnectionId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, + payload: { + ...payload, + externalMessageId: normalizedMessageLink.externalMessageId, + }, }) + yield* connectionRepo.updateLastSyncedAt(syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) - const syncHazelMessageCreateToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelMessageCreateToAllConnections", - )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { - const messageOption = yield* messageRepo.findById(hazelMessageId) - if (Option.isNone(messageOption)) { - return { synced: 0, failed: 0 } - } - const targets = yield* getActiveOutboundTargets(messageOption.value.channelId, provider) - let synced = 0 - let failed = 0 - - for (const target of targets) { - if (target.direction === "external_to_hazel") continue - const result = yield* syncHazelMessageToProvider( - target.syncConnectionId, - hazelMessageId, - dedupeKey, - ).pipe(Effect.result) - if (result._tag === "Success") { - if (result.success.status === "synced" || result.success.status === "already_linked") { - synced++ - } - } else { - failed++ - yield* Effect.logWarning("Failed to sync create message to provider", { - provider, - hazelMessageId, - syncConnectionId: target.syncConnectionId, - error: result.failure, - }) + return { + status: "deleted" as const, + externalMessageId: normalizedMessageLink.externalMessageId, + } + }) + + const getActiveOutboundTargets = Effect.fn("discordSyncWorker.getActiveOutboundTargets")(function* ( + hazelChannelId: ChannelId, + provider: ChatSyncProvider, + ) { + const targets = yield* db.execute((client) => + client + .select({ + syncConnectionId: schema.chatSyncConnectionsTable.id, + channelLinkId: schema.chatSyncChannelLinksTable.id, + direction: schema.chatSyncChannelLinksTable.direction, + }) + .from(schema.chatSyncChannelLinksTable) + .innerJoin( + schema.chatSyncConnectionsTable, + eq(schema.chatSyncConnectionsTable.id, schema.chatSyncChannelLinksTable.syncConnectionId), + ) + .where( + and( + eq(schema.chatSyncChannelLinksTable.hazelChannelId, hazelChannelId), + eq(schema.chatSyncChannelLinksTable.isActive, true), + isNull(schema.chatSyncChannelLinksTable.deletedAt), + eq(schema.chatSyncConnectionsTable.provider, provider), + eq(schema.chatSyncConnectionsTable.status, "active"), + isNull(schema.chatSyncConnectionsTable.deletedAt), + ), + ), + ) + return targets + }) + + const syncHazelMessageCreateToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelMessageCreateToAllConnections", + )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { + const messageOption = yield* messageRepo.findById(hazelMessageId) + if (Option.isNone(messageOption)) { + return { synced: 0, failed: 0 } + } + const targets = yield* getActiveOutboundTargets(messageOption.value.channelId, provider) + let synced = 0 + let failed = 0 + + for (const target of targets) { + if (target.direction === "external_to_hazel") continue + const result = yield* syncHazelMessageToProvider( + target.syncConnectionId, + hazelMessageId, + dedupeKey, + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.success.status === "synced" || result.success.status === "already_linked") { + synced++ } + } else { + failed++ + yield* Effect.logWarning("Failed to sync create message to provider", { + provider, + hazelMessageId, + syncConnectionId: target.syncConnectionId, + error: result.failure, + }) } + } - return { synced, failed } - }) + return { synced, failed } + }) - const syncHazelMessageUpdateToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelMessageUpdateToAllConnections", - )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { - const messageOption = yield* messageRepo.findById(hazelMessageId) - if (Option.isNone(messageOption)) { - return { synced: 0, failed: 0 } - } - const targets = yield* getActiveOutboundTargets(messageOption.value.channelId, provider) - let synced = 0 - let failed = 0 - - for (const target of targets) { - if (target.direction === "external_to_hazel") continue - const result = yield* syncHazelMessageUpdateToProvider( - target.syncConnectionId, - hazelMessageId, - dedupeKey, - ).pipe(Effect.result) - if (result._tag === "Success") { - if (result.success.status === "updated") { - synced++ - } - } else { - failed++ - yield* Effect.logWarning("Failed to sync update message to provider", { - provider, - hazelMessageId, - syncConnectionId: target.syncConnectionId, - error: result.failure, - }) + const syncHazelMessageUpdateToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelMessageUpdateToAllConnections", + )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { + const messageOption = yield* messageRepo.findById(hazelMessageId) + if (Option.isNone(messageOption)) { + return { synced: 0, failed: 0 } + } + const targets = yield* getActiveOutboundTargets(messageOption.value.channelId, provider) + let synced = 0 + let failed = 0 + + for (const target of targets) { + if (target.direction === "external_to_hazel") continue + const result = yield* syncHazelMessageUpdateToProvider( + target.syncConnectionId, + hazelMessageId, + dedupeKey, + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.success.status === "updated") { + synced++ } + } else { + failed++ + yield* Effect.logWarning("Failed to sync update message to provider", { + provider, + hazelMessageId, + syncConnectionId: target.syncConnectionId, + error: result.failure, + }) } + } - return { synced, failed } - }) + return { synced, failed } + }) - const syncHazelMessageDeleteToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelMessageDeleteToAllConnections", - )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { - const messageOption = yield* messageRepo.findById(hazelMessageId) - if (Option.isNone(messageOption)) { - return { synced: 0, failed: 0 } - } - const targets = yield* getActiveOutboundTargets(messageOption.value.channelId, provider) - let synced = 0 - let failed = 0 - - for (const target of targets) { - if (target.direction === "external_to_hazel") continue - const result = yield* syncHazelMessageDeleteToProvider( - target.syncConnectionId, - hazelMessageId, - dedupeKey, - ).pipe(Effect.result) - if (result._tag === "Success") { - if (result.success.status === "deleted") { - synced++ - } - } else { - failed++ - yield* Effect.logWarning("Failed to sync delete message to provider", { - provider, - hazelMessageId, - syncConnectionId: target.syncConnectionId, - error: result.failure, - }) + const syncHazelMessageDeleteToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelMessageDeleteToAllConnections", + )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { + const messageOption = yield* messageRepo.findById(hazelMessageId) + if (Option.isNone(messageOption)) { + return { synced: 0, failed: 0 } + } + const targets = yield* getActiveOutboundTargets(messageOption.value.channelId, provider) + let synced = 0 + let failed = 0 + + for (const target of targets) { + if (target.direction === "external_to_hazel") continue + const result = yield* syncHazelMessageDeleteToProvider( + target.syncConnectionId, + hazelMessageId, + dedupeKey, + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.success.status === "deleted") { + synced++ } + } else { + failed++ + yield* Effect.logWarning("Failed to sync delete message to provider", { + provider, + hazelMessageId, + syncConnectionId: target.syncConnectionId, + error: result.failure, + }) } + } - return { synced, failed } - }) + return { synced, failed } + }) - const syncHazelReactionCreateToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelReactionCreateToAllConnections", - )(function* (provider: ChatSyncProvider, hazelReactionId: MessageReactionId, dedupeKey?: string) { - const reactionOption = yield* messageReactionRepo.findById(hazelReactionId) - if (Option.isNone(reactionOption)) { - return { synced: 0, failed: 0 } - } - const reaction = reactionOption.value - const targets = yield* getActiveOutboundTargets(reaction.channelId, provider) - let synced = 0 - let failed = 0 - - for (const target of targets) { - if (target.direction === "external_to_hazel") continue - const result = yield* syncHazelReactionCreateToProvider( - target.syncConnectionId, - hazelReactionId, - dedupeKey, - ).pipe(Effect.result) - if (result._tag === "Success") { - if (result.success.status === "created") { - synced++ - } - } else { - failed++ - yield* Effect.logWarning("Failed to sync create reaction to provider", { - provider, - hazelReactionId, - syncConnectionId: target.syncConnectionId, - error: result.failure, - }) + const syncHazelReactionCreateToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelReactionCreateToAllConnections", + )(function* (provider: ChatSyncProvider, hazelReactionId: MessageReactionId, dedupeKey?: string) { + const reactionOption = yield* messageReactionRepo.findById(hazelReactionId) + if (Option.isNone(reactionOption)) { + return { synced: 0, failed: 0 } + } + const reaction = reactionOption.value + const targets = yield* getActiveOutboundTargets(reaction.channelId, provider) + let synced = 0 + let failed = 0 + + for (const target of targets) { + if (target.direction === "external_to_hazel") continue + const result = yield* syncHazelReactionCreateToProvider( + target.syncConnectionId, + hazelReactionId, + dedupeKey, + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.success.status === "created") { + synced++ } + } else { + failed++ + yield* Effect.logWarning("Failed to sync create reaction to provider", { + provider, + hazelReactionId, + syncConnectionId: target.syncConnectionId, + error: result.failure, + }) } + } - return { synced, failed } - }) + return { synced, failed } + }) - const syncHazelReactionDeleteToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelReactionDeleteToAllConnections", - )(function* ( - provider: ChatSyncProvider, - payload: { - hazelChannelId: ChannelId - hazelMessageId: MessageId - emoji: string - userId?: UserId - }, - dedupeKey?: string, - ) { - const targets = yield* getActiveOutboundTargets(payload.hazelChannelId, provider) - let synced = 0 - let failed = 0 - - for (const target of targets) { - if (target.direction === "external_to_hazel") continue - const result = yield* syncHazelReactionDeleteToProvider( - target.syncConnectionId, - payload, - dedupeKey, - ).pipe(Effect.result) - if (result._tag === "Success") { - if (result.success.status === "deleted") { - synced++ - } - } else { - failed++ - yield* Effect.logWarning("Failed to sync delete reaction to provider", { - provider, - hazelMessageId: payload.hazelMessageId, - syncConnectionId: target.syncConnectionId, - error: result.failure, - }) + const syncHazelReactionDeleteToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelReactionDeleteToAllConnections", + )(function* ( + provider: ChatSyncProvider, + payload: { + hazelChannelId: ChannelId + hazelMessageId: MessageId + emoji: string + userId?: UserId + }, + dedupeKey?: string, + ) { + const targets = yield* getActiveOutboundTargets(payload.hazelChannelId, provider) + let synced = 0 + let failed = 0 + + for (const target of targets) { + if (target.direction === "external_to_hazel") continue + const result = yield* syncHazelReactionDeleteToProvider( + target.syncConnectionId, + payload, + dedupeKey, + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.success.status === "deleted") { + synced++ } + } else { + failed++ + yield* Effect.logWarning("Failed to sync delete reaction to provider", { + provider, + hazelMessageId: payload.hazelMessageId, + syncConnectionId: target.syncConnectionId, + error: result.failure, + }) } + } - return { synced, failed } + return { synced, failed } + }) + + const syncAllActiveConnections = Effect.fn("discordSyncWorker.syncAllActiveConnections")(function* ( + provider: ChatSyncProvider, + maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, + ) { + const connections = yield* connectionRepo.findActiveByProvider(provider) + return yield* Effect.forEach( + connections, + (connection) => + syncConnection(connection.id, maxMessagesPerChannel).pipe( + Effect.map((summary) => ({ + syncConnectionId: connection.id, + ...summary, + })), + ), + { concurrency: DEFAULT_CHAT_SYNC_CONCURRENCY }, + ) + }) + + const ingestMessageCreate = Effect.fn("discordSyncWorker.ingestMessageCreate")(function* ( + payload: ChatSyncIngressMessageCreate, + ) { + const dedupeKey = payload.dedupeKey ?? `external:message:create:${payload.externalMessageId}` + const claimed = yield* claimReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, }) + if (!claimed) { + return { status: "deduped" as const } + } - const syncAllActiveConnections = Effect.fn("discordSyncWorker.syncAllActiveConnections")(function* ( - provider: ChatSyncProvider, - maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, - ) { - const connections = yield* connectionRepo.findActiveByProvider(provider) - return yield* Effect.forEach( - connections, - (connection) => - syncConnection(connection.id, maxMessagesPerChannel).pipe( - Effect.map((summary) => ({ - syncConnectionId: connection.id, - ...summary, - })), - ), - { concurrency: DEFAULT_CHAT_SYNC_CONCURRENCY }, - ) - }) + const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) - const ingestMessageCreate = Effect.fn("discordSyncWorker.ingestMessageCreate")(function* ( - payload: ChatSyncIngressMessageCreate, - ) { - const dedupeKey = payload.dedupeKey ?? `external:message:create:${payload.externalMessageId}` - const claimed = yield* claimReceipt({ + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId: payload.syncConnectionId, + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, source: "external", dedupeKey, + status: "ignored", + payload, }) - if (!claimed) { - return { status: "deduped" as const } - } - - const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) - - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId: payload.syncConnectionId, - }), - ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - status: "ignored", - payload, - }) - return { status: "ignored_connection_inactive" as const } - } - yield* getProviderAdapter(connection.provider) - - const linkOption = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - payload.externalChannelId, - ) - - if (Option.isNone(linkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: payload.syncConnectionId, - externalChannelId: payload.externalChannelId, - }), - ) - } - const link = linkOption.value - if (shouldIgnoreWebhookOrigin(connection.provider, link.settings, payload.externalWebhookId)) { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - status: "ignored", - payload, - }) - return { status: "ignored_webhook_origin" as const } - } + return { status: "ignored_connection_inactive" as const } + } + yield* getProviderAdapter(connection.provider) - const existingMessageLink = yield* messageLinkRepo.findByExternalMessage( - link.id, - payload.externalMessageId, - ) + const linkOption = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + payload.externalChannelId, + ) - if (Option.isSome(existingMessageLink)) { - yield* writeReceipt({ + if (Option.isNone(linkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - status: "ignored", - }) - return { status: "already_linked" as const } - } - - const authorId = payload.externalAuthorId - ? yield* resolveAuthorUserId({ - provider: connection.provider, - organizationId: connection.organizationId, - externalUserId: payload.externalAuthorId, - displayName: payload.externalAuthorDisplayName ?? "External User", - avatarUrl: payload.externalAuthorAvatarUrl ?? null, - }) - : (yield* integrationBotService.getOrCreateBotUser( - decodeProvider(connection.provider), - connection.organizationId, - )).id - const replyToMessageId = payload.externalReplyToMessageId - ? yield* resolveHazelMessageId({ - syncConnectionId: payload.syncConnectionId, - externalMessageId: payload.externalReplyToMessageId, - preferredChannelLinkId: link.id, - }).pipe( - Effect.map((id) => - Option.match(id, { - onNone: () => null, - onSome: (value) => value, - }), - ), - ) - : null - const normalizedExternalAttachments: Array = [] - for (const attachment of payload.externalAttachments ?? []) { - const fileName = attachment.fileName.trim() - const publicUrl = attachment.publicUrl.trim() - if (!fileName || !publicUrl) { - continue - } - normalizedExternalAttachments.push({ - externalAttachmentId: attachment.externalAttachmentId, - fileName, - fileSize: - Number.isFinite(attachment.fileSize) && attachment.fileSize >= 0 - ? attachment.fileSize - : 0, - publicUrl, - }) - } - const message = yield* db.transaction( - Effect.gen(function* () { - const [createdMessage] = yield* messageRepo.insert({ - channelId: link.hazelChannelId, - authorId, - content: payload.content, - embeds: null, - replyToMessageId, - threadChannelId: null, - deletedAt: null, - }) - - yield* outboxRepo.insert({ - eventType: "message_created", - aggregateId: createdMessage.id, - channelId: createdMessage.channelId, - payload: { - messageId: createdMessage.id, - channelId: createdMessage.channelId, - authorId: createdMessage.authorId, - content: createdMessage.content, - replyToMessageId: createdMessage.replyToMessageId, - }, - }) - - if (normalizedExternalAttachments.length > 0) { - const uploadedAtBase = Date.now() - yield* transactionAwareExecute((client) => - client.insert(schema.attachmentsTable).values( - normalizedExternalAttachments.map((attachment, index) => ({ - organizationId: connection.organizationId, - channelId: link.hazelChannelId, - messageId: createdMessage.id, - fileName: attachment.fileName, - fileSize: attachment.fileSize, - externalUrl: attachment.publicUrl, - uploadedBy: authorId, - status: "complete" as const, - uploadedAt: new Date(uploadedAtBase + index), - deletedAt: null, - })), - ), - ) - } - - yield* messageLinkRepo.insert({ - channelLinkId: link.id, - hazelMessageId: createdMessage.id, - externalMessageId: payload.externalMessageId, - source: "external", - rootHazelMessageId: null, - rootExternalMessageId: null, - hazelThreadChannelId: createdMessage.threadChannelId, - externalThreadId: payload.externalThreadId ?? null, - deletedAt: null, - }) - - return createdMessage + externalChannelId: payload.externalChannelId, }), ) - + } + const link = linkOption.value + if (shouldIgnoreWebhookOrigin(connection.provider, link.settings, payload.externalWebhookId)) { yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, channelLinkId: link.id, source: "external", dedupeKey, + status: "ignored", payload, }) - yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) + return { status: "ignored_webhook_origin" as const } + } - return { status: "created" as const, hazelMessageId: message.id } - }) + const existingMessageLink = yield* messageLinkRepo.findByExternalMessage( + link.id, + payload.externalMessageId, + ) - const ingestMessageUpdate = Effect.fn("discordSyncWorker.ingestMessageUpdate")(function* ( - payload: ChatSyncIngressMessageUpdate, - ) { - const dedupeKey = payload.dedupeKey ?? `external:message:update:${payload.externalMessageId}` - const claimed = yield* claimReceipt({ + if (Option.isSome(existingMessageLink)) { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, source: "external", dedupeKey, + status: "ignored", }) - if (!claimed) { - return { status: "deduped" as const } - } - - const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + return { status: "already_linked" as const } + } - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId: payload.syncConnectionId, - }), - ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - yield* writeReceipt({ + const authorId = payload.externalAuthorId + ? yield* resolveAuthorUserId({ + provider: connection.provider, + organizationId: connection.organizationId, + externalUserId: payload.externalAuthorId, + displayName: payload.externalAuthorDisplayName ?? "External User", + avatarUrl: payload.externalAuthorAvatarUrl ?? null, + }) + : (yield* integrationBotService.getOrCreateBotUser( + decodeProvider(connection.provider), + connection.organizationId, + )).id + const replyToMessageId = payload.externalReplyToMessageId + ? yield* resolveHazelMessageId({ syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - status: "ignored", - payload, + externalMessageId: payload.externalReplyToMessageId, + preferredChannelLinkId: link.id, + }).pipe( + Effect.map((id) => + Option.match(id, { + onNone: () => null, + onSome: (value) => value, + }), + ), + ) + : null + const normalizedExternalAttachments: Array = [] + for (const attachment of payload.externalAttachments ?? []) { + const fileName = attachment.fileName.trim() + const publicUrl = attachment.publicUrl.trim() + if (!fileName || !publicUrl) { + continue + } + normalizedExternalAttachments.push({ + externalAttachmentId: attachment.externalAttachmentId, + fileName, + fileSize: + Number.isFinite(attachment.fileSize) && attachment.fileSize >= 0 + ? attachment.fileSize + : 0, + publicUrl, + }) + } + const message = yield* db.transaction( + Effect.gen(function* () { + const [createdMessage] = yield* messageRepo.insert({ + channelId: link.hazelChannelId, + authorId, + content: payload.content, + embeds: null, + replyToMessageId, + threadChannelId: null, + deletedAt: null, }) - return { status: "ignored_connection_inactive" as const } - } - yield* getProviderAdapter(connection.provider) - - const linkOption = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - payload.externalChannelId, - ) - if (Option.isNone(linkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: payload.syncConnectionId, - externalChannelId: payload.externalChannelId, - }), - ) - } - const link = linkOption.value - if (shouldIgnoreWebhookOrigin(connection.provider, link.settings, payload.externalWebhookId)) { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - status: "ignored", - payload, + yield* outboxRepo.insert({ + eventType: "message_created", + aggregateId: createdMessage.id, + channelId: createdMessage.channelId, + payload: { + messageId: createdMessage.id, + channelId: createdMessage.channelId, + authorId: createdMessage.authorId, + content: createdMessage.content, + replyToMessageId: createdMessage.replyToMessageId, + }, }) - return { status: "ignored_webhook_origin" as const } - } - const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( - link.id, - payload.externalMessageId, - ) + if (normalizedExternalAttachments.length > 0) { + const uploadedAtBase = Date.now() + yield* transactionAwareExecute((client) => + client.insert(schema.attachmentsTable).values( + normalizedExternalAttachments.map((attachment, index) => ({ + organizationId: connection.organizationId, + channelId: link.hazelChannelId, + messageId: createdMessage.id, + fileName: attachment.fileName, + fileSize: attachment.fileSize, + externalUrl: attachment.publicUrl, + uploadedBy: authorId, + status: "complete" as const, + uploadedAt: new Date(uploadedAtBase + index), + deletedAt: null, + })), + ), + ) + } - if (Option.isNone(messageLinkOption)) { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, + yield* messageLinkRepo.insert({ channelLinkId: link.id, + hazelMessageId: createdMessage.id, + externalMessageId: payload.externalMessageId, source: "external", - dedupeKey, - status: "ignored", - payload, + rootHazelMessageId: null, + rootExternalMessageId: null, + hazelThreadChannelId: createdMessage.threadChannelId, + externalThreadId: payload.externalThreadId ?? null, + deletedAt: null, }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - yield* db.transaction( - Effect.gen(function* () { - const updatedMessage = yield* messageRepo.update({ - id: messageLink.hazelMessageId, - content: payload.content, - updatedAt: new Date(), - }) - yield* outboxRepo.insert({ - eventType: "message_updated", - aggregateId: updatedMessage.id, - channelId: updatedMessage.channelId, - payload: { - messageId: updatedMessage.id, - }, - }) + return createdMessage + }), + ) + + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, + source: "external", + dedupeKey, + payload, + }) + yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) + + return { status: "created" as const, hazelMessageId: message.id } + }) + + const ingestMessageUpdate = Effect.fn("discordSyncWorker.ingestMessageUpdate")(function* ( + payload: ChatSyncIngressMessageUpdate, + ) { + const dedupeKey = payload.dedupeKey ?? `external:message:update:${payload.externalMessageId}` + const claimed = yield* claimReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + }) + if (!claimed) { + return { status: "deduped" as const } + } + + const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId: payload.syncConnectionId, }), ) - + } + const connection = connectionOption.value + if (connection.status !== "active") { yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, source: "external", dedupeKey, + status: "ignored", payload, }) - yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) + return { status: "ignored_connection_inactive" as const } + } + yield* getProviderAdapter(connection.provider) - return { status: "updated" as const, hazelMessageId: messageLink.hazelMessageId } - }) + const linkOption = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + payload.externalChannelId, + ) - const ingestMessageDelete = Effect.fn("discordSyncWorker.ingestMessageDelete")(function* ( - payload: ChatSyncIngressMessageDelete, - ) { - const dedupeKey = payload.dedupeKey ?? `external:message:delete:${payload.externalMessageId}` - const claimed = yield* claimReceipt({ + if (Option.isNone(linkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: payload.syncConnectionId, + externalChannelId: payload.externalChannelId, + }), + ) + } + const link = linkOption.value + if (shouldIgnoreWebhookOrigin(connection.provider, link.settings, payload.externalWebhookId)) { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, source: "external", dedupeKey, + status: "ignored", + payload, }) - if (!claimed) { - return { status: "deduped" as const } - } + return { status: "ignored_webhook_origin" as const } + } - const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( + link.id, + payload.externalMessageId, + ) - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId: payload.syncConnectionId, - }), - ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - status: "ignored", - payload, + if (Option.isNone(messageLinkOption)) { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + + yield* db.transaction( + Effect.gen(function* () { + const updatedMessage = yield* messageRepo.update({ + id: messageLink.hazelMessageId, + content: payload.content, + updatedAt: new Date(), }) - return { status: "ignored_connection_inactive" as const } - } - yield* getProviderAdapter(connection.provider) - - const linkOption = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - payload.externalChannelId, - ) - - if (Option.isNone(linkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: payload.syncConnectionId, - externalChannelId: payload.externalChannelId, - }), - ) - } - const link = linkOption.value - if (shouldIgnoreWebhookOrigin(connection.provider, link.settings, payload.externalWebhookId)) { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - status: "ignored", - payload, + yield* outboxRepo.insert({ + eventType: "message_updated", + aggregateId: updatedMessage.id, + channelId: updatedMessage.channelId, + payload: { + messageId: updatedMessage.id, + }, }) - return { status: "ignored_webhook_origin" as const } - } + }), + ) - const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( - link.id, - payload.externalMessageId, - ) + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, + source: "external", + dedupeKey, + payload, + }) + yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) + + return { status: "updated" as const, hazelMessageId: messageLink.hazelMessageId } + }) + + const ingestMessageDelete = Effect.fn("discordSyncWorker.ingestMessageDelete")(function* ( + payload: ChatSyncIngressMessageDelete, + ) { + const dedupeKey = payload.dedupeKey ?? `external:message:delete:${payload.externalMessageId}` + const claimed = yield* claimReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + }) + if (!claimed) { + return { status: "deduped" as const } + } - if (Option.isNone(messageLinkOption)) { - yield* writeReceipt({ + const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - status: "ignored", - payload, - }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "ignored_connection_inactive" as const } + } + yield* getProviderAdapter(connection.provider) - yield* db.transaction( - Effect.gen(function* () { - const deletedMessage = yield* messageRepo.update({ - id: messageLink.hazelMessageId, - deletedAt: new Date(), - updatedAt: new Date(), - }) - yield* outboxRepo.insert({ - eventType: "message_deleted", - aggregateId: deletedMessage.id, - channelId: deletedMessage.channelId, - payload: { - messageId: deletedMessage.id, - channelId: deletedMessage.channelId, - }, - }) + const linkOption = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + payload.externalChannelId, + ) + + if (Option.isNone(linkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: payload.syncConnectionId, + externalChannelId: payload.externalChannelId, }), ) - + } + const link = linkOption.value + if (shouldIgnoreWebhookOrigin(connection.provider, link.settings, payload.externalWebhookId)) { yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, channelLinkId: link.id, source: "external", dedupeKey, + status: "ignored", payload, }) - yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) + return { status: "ignored_webhook_origin" as const } + } - return { status: "deleted" as const, hazelMessageId: messageLink.hazelMessageId } - }) + const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( + link.id, + payload.externalMessageId, + ) - const ingestReactionAdd = Effect.fn("discordSyncWorker.ingestReactionAdd")(function* ( - payload: ChatSyncIngressReactionAdd, - ) { - const dedupeKey = - payload.dedupeKey ?? - `external:reaction:add:${payload.externalMessageId}:${payload.externalUserId}:${payload.emoji}` - const claimed = yield* claimReceipt({ + if (Option.isNone(messageLinkOption)) { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, source: "external", dedupeKey, + status: "ignored", + payload, }) - if (!claimed) { - return { status: "deduped" as const } - } - - const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) - - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId: payload.syncConnectionId, - }), - ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - status: "ignored", - payload, + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + + yield* db.transaction( + Effect.gen(function* () { + const deletedMessage = yield* messageRepo.update({ + id: messageLink.hazelMessageId, + deletedAt: new Date(), + updatedAt: new Date(), }) - return { status: "ignored_connection_inactive" as const } - } - yield* getProviderAdapter(connection.provider) - - const linkOption = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - payload.externalChannelId, - ) + yield* outboxRepo.insert({ + eventType: "message_deleted", + aggregateId: deletedMessage.id, + channelId: deletedMessage.channelId, + payload: { + messageId: deletedMessage.id, + channelId: deletedMessage.channelId, + }, + }) + }), + ) - if (Option.isNone(linkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: payload.syncConnectionId, - externalChannelId: payload.externalChannelId, - }), - ) - } - const link = linkOption.value + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, + source: "external", + dedupeKey, + payload, + }) + yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) + + return { status: "deleted" as const, hazelMessageId: messageLink.hazelMessageId } + }) + + const ingestReactionAdd = Effect.fn("discordSyncWorker.ingestReactionAdd")(function* ( + payload: ChatSyncIngressReactionAdd, + ) { + const dedupeKey = + payload.dedupeKey ?? + `external:reaction:add:${payload.externalMessageId}:${payload.externalUserId}:${payload.emoji}` + const claimed = yield* claimReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + }) + if (!claimed) { + return { status: "deduped" as const } + } - const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( - link.id, - payload.externalMessageId, - ) + const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) - if (Option.isNone(messageLinkOption)) { - yield* writeReceipt({ + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - status: "ignored", - payload, - }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - - const userId = yield* resolveAuthorUserId({ - provider: connection.provider, - organizationId: connection.organizationId, - externalUserId: payload.externalUserId, - displayName: payload.externalAuthorDisplayName ?? "Discord User", - avatarUrl: payload.externalAuthorAvatarUrl ?? null, + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + status: "ignored", + payload, }) + return { status: "ignored_connection_inactive" as const } + } + yield* getProviderAdapter(connection.provider) - const existingReaction = yield* messageReactionRepo.findByMessageUserEmoji( - messageLink.hazelMessageId, - userId, - payload.emoji, - ) + const linkOption = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + payload.externalChannelId, + ) - if (Option.isSome(existingReaction)) { - yield* writeReceipt({ + if (Option.isNone(linkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - status: "ignored", - payload, - }) - return { status: "already_exists" as const } - } - - const reaction = yield* db.transaction( - Effect.gen(function* () { - const [createdReaction] = yield* messageReactionRepo.insert({ - messageId: messageLink.hazelMessageId, - channelId: link.hazelChannelId, - userId, - emoji: payload.emoji, - }) - yield* outboxRepo.insert({ - eventType: "reaction_created", - aggregateId: createdReaction.id, - channelId: createdReaction.channelId, - payload: { - reactionId: createdReaction.id, - }, - }) - return createdReaction + externalChannelId: payload.externalChannelId, }), ) + } + const link = linkOption.value + + const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( + link.id, + payload.externalMessageId, + ) + if (Option.isNone(messageLinkOption)) { yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, channelLinkId: link.id, source: "external", dedupeKey, + status: "ignored", payload, }) - yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) - - return { status: "created" as const, hazelReactionId: reaction.id } + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + + const userId = yield* resolveAuthorUserId({ + provider: connection.provider, + organizationId: connection.organizationId, + externalUserId: payload.externalUserId, + displayName: payload.externalAuthorDisplayName ?? "Discord User", + avatarUrl: payload.externalAuthorAvatarUrl ?? null, }) - const ingestReactionRemove = Effect.fn("discordSyncWorker.ingestReactionRemove")(function* ( - payload: ChatSyncIngressReactionRemove, - ) { - const dedupeKey = - payload.dedupeKey ?? - `external:reaction:remove:${payload.externalMessageId}:${payload.externalUserId}:${payload.emoji}` - const claimed = yield* claimReceipt({ + const existingReaction = yield* messageReactionRepo.findByMessageUserEmoji( + messageLink.hazelMessageId, + userId, + payload.emoji, + ) + + if (Option.isSome(existingReaction)) { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, source: "external", dedupeKey, + status: "ignored", + payload, }) - if (!claimed) { - return { status: "deduped" as const } - } - - const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + return { status: "already_exists" as const } + } - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId: payload.syncConnectionId, - }), - ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - status: "ignored", - payload, + const reaction = yield* db.transaction( + Effect.gen(function* () { + const [createdReaction] = yield* messageReactionRepo.insert({ + messageId: messageLink.hazelMessageId, + channelId: link.hazelChannelId, + userId, + emoji: payload.emoji, }) - return { status: "ignored_connection_inactive" as const } - } - yield* getProviderAdapter(connection.provider) - - const linkOption = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - payload.externalChannelId, - ) + yield* outboxRepo.insert({ + eventType: "reaction_created", + aggregateId: createdReaction.id, + channelId: createdReaction.channelId, + payload: { + reactionId: createdReaction.id, + }, + }) + return createdReaction + }), + ) - if (Option.isNone(linkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: payload.syncConnectionId, - externalChannelId: payload.externalChannelId, - }), - ) - } - const link = linkOption.value + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, + source: "external", + dedupeKey, + payload, + }) + yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) + + return { status: "created" as const, hazelReactionId: reaction.id } + }) + + const ingestReactionRemove = Effect.fn("discordSyncWorker.ingestReactionRemove")(function* ( + payload: ChatSyncIngressReactionRemove, + ) { + const dedupeKey = + payload.dedupeKey ?? + `external:reaction:remove:${payload.externalMessageId}:${payload.externalUserId}:${payload.emoji}` + const claimed = yield* claimReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + }) + if (!claimed) { + return { status: "deduped" as const } + } - const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( - link.id, - payload.externalMessageId, - ) + const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) - if (Option.isNone(messageLinkOption)) { - yield* writeReceipt({ + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - status: "ignored", - payload, - }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - - const userId = yield* resolveAuthorUserId({ - provider: connection.provider, - organizationId: connection.organizationId, - externalUserId: payload.externalUserId, - displayName: payload.externalAuthorDisplayName ?? "Discord User", - avatarUrl: payload.externalAuthorAvatarUrl ?? null, + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + status: "ignored", + payload, }) + return { status: "ignored_connection_inactive" as const } + } + yield* getProviderAdapter(connection.provider) - const existingReaction = yield* messageReactionRepo.findByMessageUserEmoji( - messageLink.hazelMessageId, - userId, - payload.emoji, - ) + const linkOption = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + payload.externalChannelId, + ) - if (Option.isNone(existingReaction)) { - yield* writeReceipt({ + if (Option.isNone(linkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - status: "ignored", - payload, - }) - return { status: "already_deleted" as const } - } - - yield* db.transaction( - Effect.gen(function* () { - yield* messageReactionRepo.deleteById(existingReaction.value.id) - yield* outboxRepo.insert({ - eventType: "reaction_deleted", - aggregateId: existingReaction.value.id, - channelId: link.hazelChannelId, - payload: { - hazelChannelId: link.hazelChannelId, - hazelMessageId: messageLink.hazelMessageId, - emoji: payload.emoji, - userId, - }, - }) + externalChannelId: payload.externalChannelId, }), ) + } + const link = linkOption.value + const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( + link.id, + payload.externalMessageId, + ) + + if (Option.isNone(messageLinkOption)) { yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, channelLinkId: link.id, source: "external", dedupeKey, + status: "ignored", payload, }) - yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) - - return { status: "deleted" as const, hazelReactionId: existingReaction.value.id } + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + + const userId = yield* resolveAuthorUserId({ + provider: connection.provider, + organizationId: connection.organizationId, + externalUserId: payload.externalUserId, + displayName: payload.externalAuthorDisplayName ?? "Discord User", + avatarUrl: payload.externalAuthorAvatarUrl ?? null, }) - const ingestThreadCreate = Effect.fn("discordSyncWorker.ingestThreadCreate")(function* ( - payload: ChatSyncIngressThreadCreate, - ) { - const dedupeKey = payload.dedupeKey ?? `external:thread:create:${payload.externalThreadId}` - const claimed = yield* claimReceipt({ + const existingReaction = yield* messageReactionRepo.findByMessageUserEmoji( + messageLink.hazelMessageId, + userId, + payload.emoji, + ) + + if (Option.isNone(existingReaction)) { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, source: "external", dedupeKey, + status: "ignored", + payload, }) - if (!claimed) { - return { status: "deduped" as const } - } - - const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + return { status: "already_deleted" as const } + } - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId: payload.syncConnectionId, - }), - ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - status: "ignored", - payload, + yield* db.transaction( + Effect.gen(function* () { + yield* messageReactionRepo.deleteById(existingReaction.value.id) + yield* outboxRepo.insert({ + eventType: "reaction_deleted", + aggregateId: existingReaction.value.id, + channelId: link.hazelChannelId, + payload: { + hazelChannelId: link.hazelChannelId, + hazelMessageId: messageLink.hazelMessageId, + emoji: payload.emoji, + userId, + }, }) - return { status: "ignored_connection_inactive" as const } - } - yield* getProviderAdapter(connection.provider) - - const parentLinkOption = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - payload.externalParentChannelId, - ) + }), + ) - if (Option.isNone(parentLinkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: payload.syncConnectionId, - externalChannelId: payload.externalParentChannelId, - }), - ) - } - const parentLink = parentLinkOption.value - const externalThreadChannelId = payload.externalThreadId as unknown as ExternalChannelId + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, + source: "external", + dedupeKey, + payload, + }) + yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) + + return { status: "deleted" as const, hazelReactionId: existingReaction.value.id } + }) + + const ingestThreadCreate = Effect.fn("discordSyncWorker.ingestThreadCreate")(function* ( + payload: ChatSyncIngressThreadCreate, + ) { + const dedupeKey = payload.dedupeKey ?? `external:thread:create:${payload.externalThreadId}` + const claimed = yield* claimReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + }) + if (!claimed) { + return { status: "deduped" as const } + } - const existingThreadLink = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - externalThreadChannelId, - ) + const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) - if (Option.isSome(existingThreadLink)) { - yield* writeReceipt({ + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ syncConnectionId: payload.syncConnectionId, - channelLinkId: existingThreadLink.value.id, - source: "external", - dedupeKey, - status: "ignored", - payload, - }) - return { status: "already_linked" as const } - } - - const [threadChannel] = yield* channelRepo.insert({ - name: payload.name?.trim() || "Thread", - icon: null, - type: "thread", - organizationId: connection.organizationId, - parentChannelId: parentLink.hazelChannelId, - sectionId: null, - deletedAt: null, - }) - - yield* channelAccessSyncService.syncChannel(threadChannel.id) - - const [threadLink] = yield* channelLinkRepo.insert({ + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, - hazelChannelId: threadChannel.id, - externalChannelId: externalThreadChannelId, - externalChannelName: payload.name ?? null, - direction: parentLink.direction, - isActive: true, - settings: parentLink.settings, - lastSyncedAt: null, - deletedAt: null, + source: "external", + dedupeKey, + status: "ignored", + payload, }) + return { status: "ignored_connection_inactive" as const } + } + yield* getProviderAdapter(connection.provider) - if (payload.externalRootMessageId) { - const rootMessageLink = yield* messageLinkRepo.findByExternalMessage( - parentLink.id, - payload.externalRootMessageId, - ) + const parentLinkOption = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + payload.externalParentChannelId, + ) - if (Option.isSome(rootMessageLink)) { - yield* db.transaction( - Effect.gen(function* () { - const updatedMessage = yield* messageRepo.update({ - id: rootMessageLink.value.hazelMessageId, - threadChannelId: threadChannel.id, - updatedAt: new Date(), - }) - yield* outboxRepo.insert({ - eventType: "message_updated", - aggregateId: updatedMessage.id, - channelId: updatedMessage.channelId, - payload: { - messageId: updatedMessage.id, - }, - }) - }), - ) - } - } + if (Option.isNone(parentLinkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: payload.syncConnectionId, + externalChannelId: payload.externalParentChannelId, + }), + ) + } + const parentLink = parentLinkOption.value + const externalThreadChannelId = payload.externalThreadId as unknown as ExternalChannelId + const existingThreadLink = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + externalThreadChannelId, + ) + + if (Option.isSome(existingThreadLink)) { yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, - channelLinkId: threadLink.id, + channelLinkId: existingThreadLink.value.id, source: "external", dedupeKey, + status: "ignored", payload, }) - yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(threadLink.id) - yield* channelLinkRepo.updateLastSyncedAt(parentLink.id) + return { status: "already_linked" as const } + } - return { - status: "created" as const, - hazelThreadChannelId: threadChannel.id, - channelLinkId: threadLink.id, + const [threadChannel] = yield* channelRepo.insert({ + name: payload.name?.trim() || "Thread", + icon: null, + type: "thread", + organizationId: connection.organizationId, + parentChannelId: parentLink.hazelChannelId, + sectionId: null, + deletedAt: null, + }) + + yield* channelAccessSyncService.syncChannel(threadChannel.id) + + const [threadLink] = yield* channelLinkRepo.insert({ + syncConnectionId: payload.syncConnectionId, + hazelChannelId: threadChannel.id, + externalChannelId: externalThreadChannelId, + externalChannelName: payload.name ?? null, + direction: parentLink.direction, + isActive: true, + settings: parentLink.settings, + lastSyncedAt: null, + deletedAt: null, + }) + + if (payload.externalRootMessageId) { + const rootMessageLink = yield* messageLinkRepo.findByExternalMessage( + parentLink.id, + payload.externalRootMessageId, + ) + + if (Option.isSome(rootMessageLink)) { + yield* db.transaction( + Effect.gen(function* () { + const updatedMessage = yield* messageRepo.update({ + id: rootMessageLink.value.hazelMessageId, + threadChannelId: threadChannel.id, + updatedAt: new Date(), + }) + yield* outboxRepo.insert({ + eventType: "message_updated", + aggregateId: updatedMessage.id, + channelId: updatedMessage.channelId, + payload: { + messageId: updatedMessage.id, + }, + }) + }), + ) } + } + + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: threadLink.id, + source: "external", + dedupeKey, + payload, }) + yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(threadLink.id) + yield* channelLinkRepo.updateLastSyncedAt(parentLink.id) return { - syncConnection, - syncAllActiveConnections, - syncHazelMessageToProvider, - syncHazelMessageUpdateToProvider, - syncHazelMessageDeleteToProvider, - syncHazelReactionCreateToProvider, - syncHazelReactionDeleteToProvider, - syncHazelMessageCreateToAllConnections, - syncHazelMessageUpdateToAllConnections, - syncHazelMessageDeleteToAllConnections, - syncHazelReactionCreateToAllConnections, - syncHazelReactionDeleteToAllConnections, - ingestMessageCreate, - ingestMessageUpdate, - ingestMessageDelete, - ingestReactionAdd, - ingestReactionRemove, - ingestThreadCreate, - } - }), -}) { - static readonly layer = Layer.effect(this, this.make).pipe( - Layer.provide(ChatSyncConnectionRepo.layer), - Layer.provide(ChatSyncChannelLinkRepo.layer), - Layer.provide(ChatSyncMessageLinkRepo.layer), - Layer.provide(ChatSyncEventReceiptRepo.layer), - Layer.provide(MessageRepo.layer), - Layer.provide(MessageOutboxRepo.layer), - Layer.provide(MessageReactionRepo.layer), - Layer.provide(ChannelRepo.layer), - Layer.provide(IntegrationConnectionRepo.layer), - Layer.provide(UserRepo.layer), - Layer.provide(OrganizationMemberRepo.layer), - Layer.provide(IntegrationBotService.layer), - Layer.provide(ChannelAccessSyncService.layer), - Layer.provide(ChatSyncProviderRegistry.layer), - Layer.provide(Discord.DiscordApiClient.layer), - ) -} + status: "created" as const, + hazelThreadChannelId: threadChannel.id, + channelLinkId: threadLink.id, + } + }) + + return { + syncConnection, + syncAllActiveConnections, + syncHazelMessageToProvider, + syncHazelMessageUpdateToProvider, + syncHazelMessageDeleteToProvider, + syncHazelReactionCreateToProvider, + syncHazelReactionDeleteToProvider, + syncHazelMessageCreateToAllConnections, + syncHazelMessageUpdateToAllConnections, + syncHazelMessageDeleteToAllConnections, + syncHazelReactionCreateToAllConnections, + syncHazelReactionDeleteToAllConnections, + ingestMessageCreate, + ingestMessageUpdate, + ingestMessageDelete, + ingestReactionAdd, + ingestReactionRemove, + ingestThreadCreate, + } +}) + +export const ChatSyncCoreWorkerLayer = Layer.effect(ChatSyncCoreWorker, ChatSyncCoreWorkerMake).pipe( + Layer.provide(ChatSyncConnectionRepo.layer), + Layer.provide(ChatSyncChannelLinkRepo.layer), + Layer.provide(ChatSyncMessageLinkRepo.layer), + Layer.provide(ChatSyncEventReceiptRepo.layer), + Layer.provide(MessageRepo.layer), + Layer.provide(MessageOutboxRepo.layer), + Layer.provide(MessageReactionRepo.layer), + Layer.provide(ChannelRepo.layer), + Layer.provide(IntegrationConnectionRepo.layer), + Layer.provide(UserRepo.layer), + Layer.provide(OrganizationMemberRepo.layer), + Layer.provide(IntegrationBotService.layer), + Layer.provide(ChannelAccessSyncService.layer), + Layer.provide(ChatSyncProviderRegistry.layer), + Layer.provide(Discord.DiscordApiClient.layer), +) diff --git a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts index f6558b916..40285c288 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-provider-registry.ts @@ -91,9 +91,11 @@ export class ChatSyncProviderRegistry extends ServiceMap.Service { diff --git a/apps/backend/src/services/chat-sync/discord-gateway-service.ts b/apps/backend/src/services/chat-sync/discord-gateway-service.ts index 76a0067a0..0f6976dff 100644 --- a/apps/backend/src/services/chat-sync/discord-gateway-service.ts +++ b/apps/backend/src/services/chat-sync/discord-gateway-service.ts @@ -13,7 +13,7 @@ import { import { DiscordConfig } from "dfx" import { DiscordGateway, DiscordLive } from "dfx/gateway" import { ServiceMap, Config, Effect, Layer, Option, Redacted, Ref, Schema } from "effect" -import { DiscordSyncWorker } from "./discord-sync-worker" +import { DiscordSyncWorker, DiscordSyncWorkerLayer } from "./discord-sync-worker" import type { ChatSyncIngressMessageAttachment } from "./chat-sync-core-worker" export interface DiscordMessageAuthor { @@ -748,7 +748,7 @@ export class DiscordGatewayService extends ServiceMap.Service()("DiscordSyncWorker", { - make: Effect.gen(function* () { - const coreWorker = yield* ChatSyncCoreWorker - - const syncConnection = Effect.fn("DiscordSyncWorker.syncConnection")(function* ( - syncConnectionId: SyncConnectionId, - maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, - ) { - return yield* coreWorker.syncConnection(syncConnectionId, maxMessagesPerChannel) - }) - - const syncAllActiveConnections = Effect.fn("DiscordSyncWorker.syncAllActiveConnections")(function* ( - maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, - ) { - return yield* coreWorker.syncAllActiveConnections("discord", maxMessagesPerChannel) - }) - - const syncHazelMessageToDiscord = Effect.fn("DiscordSyncWorker.syncHazelMessageToDiscord")(function* ( - syncConnectionId: SyncConnectionId, - hazelMessageId: MessageId, - dedupeKeyOverride?: string, - ) { - return yield* coreWorker.syncHazelMessageToProvider( - syncConnectionId, - hazelMessageId, - dedupeKeyOverride, - ) - }) - - const syncHazelMessageUpdateToDiscord = Effect.fn( - "discordSyncWorker.syncHazelMessageUpdateToDiscord", - )(function* ( +export class DiscordSyncWorker extends ServiceMap.Service()("DiscordSyncWorker") {} + +const DiscordSyncWorkerMake = Effect.gen(function* () { + const coreWorker = yield* ChatSyncCoreWorker + + const syncConnection = Effect.fn("DiscordSyncWorker.syncConnection")(function* ( + syncConnectionId: SyncConnectionId, + maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, + ) { + return yield* coreWorker.syncConnection(syncConnectionId, maxMessagesPerChannel) + }) + + const syncAllActiveConnections = Effect.fn("DiscordSyncWorker.syncAllActiveConnections")(function* ( + maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, + ) { + return yield* coreWorker.syncAllActiveConnections("discord", maxMessagesPerChannel) + }) + + const syncHazelMessageToDiscord = Effect.fn("DiscordSyncWorker.syncHazelMessageToDiscord")(function* ( + syncConnectionId: SyncConnectionId, + hazelMessageId: MessageId, + dedupeKeyOverride?: string, + ) { + return yield* coreWorker.syncHazelMessageToProvider( + syncConnectionId, + hazelMessageId, + dedupeKeyOverride, + ) + }) + + const syncHazelMessageUpdateToDiscord = Effect.fn("discordSyncWorker.syncHazelMessageUpdateToDiscord")( + function* ( syncConnectionId: SyncConnectionId, hazelMessageId: MessageId, dedupeKeyOverride?: string, @@ -72,11 +73,11 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" hazelMessageId, dedupeKeyOverride, ) - }) + }, + ) - const syncHazelMessageDeleteToDiscord = Effect.fn( - "discordSyncWorker.syncHazelMessageDeleteToDiscord", - )(function* ( + const syncHazelMessageDeleteToDiscord = Effect.fn("discordSyncWorker.syncHazelMessageDeleteToDiscord")( + function* ( syncConnectionId: SyncConnectionId, hazelMessageId: MessageId, dedupeKeyOverride?: string, @@ -86,11 +87,11 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" hazelMessageId, dedupeKeyOverride, ) - }) + }, + ) - const syncHazelReactionCreateToDiscord = Effect.fn( - "discordSyncWorker.syncHazelReactionCreateToDiscord", - )(function* ( + const syncHazelReactionCreateToDiscord = Effect.fn("discordSyncWorker.syncHazelReactionCreateToDiscord")( + function* ( syncConnectionId: SyncConnectionId, hazelReactionId: MessageReactionId, dedupeKeyOverride?: string, @@ -100,11 +101,11 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" hazelReactionId, dedupeKeyOverride, ) - }) + }, + ) - const syncHazelReactionDeleteToDiscord = Effect.fn( - "discordSyncWorker.syncHazelReactionDeleteToDiscord", - )(function* ( + const syncHazelReactionDeleteToDiscord = Effect.fn("discordSyncWorker.syncHazelReactionDeleteToDiscord")( + function* ( syncConnectionId: SyncConnectionId, payload: { hazelChannelId: ChannelId @@ -119,119 +120,109 @@ export class DiscordSyncWorker extends ServiceMap.Service()(" payload, dedupeKeyOverride, ) - }) - - const syncHazelMessageCreateToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelMessageCreateToAllConnections", - )(function* (hazelMessageId: MessageId, dedupeKey?: string) { - return yield* coreWorker.syncHazelMessageCreateToAllConnections( - "discord", - hazelMessageId, - dedupeKey, - ) - }) - - const syncHazelMessageUpdateToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelMessageUpdateToAllConnections", - )(function* (hazelMessageId: MessageId, dedupeKey?: string) { - return yield* coreWorker.syncHazelMessageUpdateToAllConnections( - "discord", - hazelMessageId, - dedupeKey, - ) - }) - - const syncHazelMessageDeleteToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelMessageDeleteToAllConnections", - )(function* (hazelMessageId: MessageId, dedupeKey?: string) { - return yield* coreWorker.syncHazelMessageDeleteToAllConnections( - "discord", - hazelMessageId, - dedupeKey, - ) - }) - - const syncHazelReactionCreateToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelReactionCreateToAllConnections", - )(function* (hazelReactionId: MessageReactionId, dedupeKey?: string) { - return yield* coreWorker.syncHazelReactionCreateToAllConnections( - "discord", - hazelReactionId, - dedupeKey, - ) - }) - - const syncHazelReactionDeleteToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelReactionDeleteToAllConnections", - )(function* ( - payload: { - hazelChannelId: ChannelId - hazelMessageId: MessageId - emoji: string - userId?: UserId - }, - dedupeKey?: string, - ) { - return yield* coreWorker.syncHazelReactionDeleteToAllConnections("discord", payload, dedupeKey) - }) - - const ingestMessageCreate = Effect.fn("DiscordSyncWorker.ingestMessageCreate")(function* ( - payload: DiscordIngressMessageCreate, - ) { - return yield* coreWorker.ingestMessageCreate(payload) - }) - - const ingestMessageUpdate = Effect.fn("DiscordSyncWorker.ingestMessageUpdate")(function* ( - payload: DiscordIngressMessageUpdate, - ) { - return yield* coreWorker.ingestMessageUpdate(payload) - }) - - const ingestMessageDelete = Effect.fn("DiscordSyncWorker.ingestMessageDelete")(function* ( - payload: DiscordIngressMessageDelete, - ) { - return yield* coreWorker.ingestMessageDelete(payload) - }) - - const ingestReactionAdd = Effect.fn("DiscordSyncWorker.ingestReactionAdd")(function* ( - payload: DiscordIngressReactionAdd, - ) { - return yield* coreWorker.ingestReactionAdd(payload) - }) - - const ingestReactionRemove = Effect.fn("DiscordSyncWorker.ingestReactionRemove")(function* ( - payload: DiscordIngressReactionRemove, - ) { - return yield* coreWorker.ingestReactionRemove(payload) - }) - - const ingestThreadCreate = Effect.fn("DiscordSyncWorker.ingestThreadCreate")(function* ( - payload: DiscordIngressThreadCreate, - ) { - return yield* coreWorker.ingestThreadCreate(payload) - }) - - return { - syncConnection, - syncAllActiveConnections, - syncHazelMessageToDiscord, - syncHazelMessageUpdateToDiscord, - syncHazelMessageDeleteToDiscord, - syncHazelReactionCreateToDiscord, - syncHazelReactionDeleteToDiscord, - syncHazelMessageCreateToAllConnections, - syncHazelMessageUpdateToAllConnections, - syncHazelMessageDeleteToAllConnections, - syncHazelReactionCreateToAllConnections, - syncHazelReactionDeleteToAllConnections, - ingestMessageCreate, - ingestMessageUpdate, - ingestMessageDelete, - ingestReactionAdd, - ingestReactionRemove, - ingestThreadCreate, - } - }), -}) { - static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(ChatSyncCoreWorker.layer)) -} + }, + ) + + const syncHazelMessageCreateToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelMessageCreateToAllConnections", + )(function* (hazelMessageId: MessageId, dedupeKey?: string) { + return yield* coreWorker.syncHazelMessageCreateToAllConnections("discord", hazelMessageId, dedupeKey) + }) + + const syncHazelMessageUpdateToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelMessageUpdateToAllConnections", + )(function* (hazelMessageId: MessageId, dedupeKey?: string) { + return yield* coreWorker.syncHazelMessageUpdateToAllConnections("discord", hazelMessageId, dedupeKey) + }) + + const syncHazelMessageDeleteToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelMessageDeleteToAllConnections", + )(function* (hazelMessageId: MessageId, dedupeKey?: string) { + return yield* coreWorker.syncHazelMessageDeleteToAllConnections("discord", hazelMessageId, dedupeKey) + }) + + const syncHazelReactionCreateToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelReactionCreateToAllConnections", + )(function* (hazelReactionId: MessageReactionId, dedupeKey?: string) { + return yield* coreWorker.syncHazelReactionCreateToAllConnections( + "discord", + hazelReactionId, + dedupeKey, + ) + }) + + const syncHazelReactionDeleteToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelReactionDeleteToAllConnections", + )(function* ( + payload: { + hazelChannelId: ChannelId + hazelMessageId: MessageId + emoji: string + userId?: UserId + }, + dedupeKey?: string, + ) { + return yield* coreWorker.syncHazelReactionDeleteToAllConnections("discord", payload, dedupeKey) + }) + + const ingestMessageCreate = Effect.fn("DiscordSyncWorker.ingestMessageCreate")(function* ( + payload: DiscordIngressMessageCreate, + ) { + return yield* coreWorker.ingestMessageCreate(payload) + }) + + const ingestMessageUpdate = Effect.fn("DiscordSyncWorker.ingestMessageUpdate")(function* ( + payload: DiscordIngressMessageUpdate, + ) { + return yield* coreWorker.ingestMessageUpdate(payload) + }) + + const ingestMessageDelete = Effect.fn("DiscordSyncWorker.ingestMessageDelete")(function* ( + payload: DiscordIngressMessageDelete, + ) { + return yield* coreWorker.ingestMessageDelete(payload) + }) + + const ingestReactionAdd = Effect.fn("DiscordSyncWorker.ingestReactionAdd")(function* ( + payload: DiscordIngressReactionAdd, + ) { + return yield* coreWorker.ingestReactionAdd(payload) + }) + + const ingestReactionRemove = Effect.fn("DiscordSyncWorker.ingestReactionRemove")(function* ( + payload: DiscordIngressReactionRemove, + ) { + return yield* coreWorker.ingestReactionRemove(payload) + }) + + const ingestThreadCreate = Effect.fn("DiscordSyncWorker.ingestThreadCreate")(function* ( + payload: DiscordIngressThreadCreate, + ) { + return yield* coreWorker.ingestThreadCreate(payload) + }) + + return { + syncConnection, + syncAllActiveConnections, + syncHazelMessageToDiscord, + syncHazelMessageUpdateToDiscord, + syncHazelMessageDeleteToDiscord, + syncHazelReactionCreateToDiscord, + syncHazelReactionDeleteToDiscord, + syncHazelMessageCreateToAllConnections, + syncHazelMessageUpdateToAllConnections, + syncHazelMessageDeleteToAllConnections, + syncHazelReactionCreateToAllConnections, + syncHazelReactionDeleteToAllConnections, + ingestMessageCreate, + ingestMessageUpdate, + ingestMessageDelete, + ingestReactionAdd, + ingestReactionRemove, + ingestThreadCreate, + } +}) + +export const DiscordSyncWorkerLayer = Layer.effect(DiscordSyncWorker, DiscordSyncWorkerMake).pipe( + Layer.provide(ChatSyncCoreWorkerLayer), +) diff --git a/apps/backend/src/services/integration-token-service.ts b/apps/backend/src/services/integration-token-service.ts index a5817a7d3..20dc9947b 100644 --- a/apps/backend/src/services/integration-token-service.ts +++ b/apps/backend/src/services/integration-token-service.ts @@ -223,8 +223,8 @@ export class IntegrationTokenService extends ServiceMap.Service diff --git a/apps/backend/src/services/message-side-effect-service.ts b/apps/backend/src/services/message-side-effect-service.ts index 8a900b235..373a8cbf7 100644 --- a/apps/backend/src/services/message-side-effect-service.ts +++ b/apps/backend/src/services/message-side-effect-service.ts @@ -9,7 +9,7 @@ import type { ReactionCreatedPayload, ReactionDeletedPayload, } from "@hazel/backend-core" -import { DiscordSyncWorker } from "./chat-sync/discord-sync-worker" +import { DiscordSyncWorker, DiscordSyncWorkerLayer } from "./chat-sync/discord-sync-worker" export class MessageSideEffectService extends ServiceMap.Service()( "MessageSideEffectService", @@ -259,5 +259,5 @@ export class MessageSideEffectService extends ServiceMap.Service new ProviderNotConfiguredError({ diff --git a/apps/backend/src/services/oauth/providers/discord-oauth-provider.ts b/apps/backend/src/services/oauth/providers/discord-oauth-provider.ts index ed2d7df19..fffc59fb4 100644 --- a/apps/backend/src/services/oauth/providers/discord-oauth-provider.ts +++ b/apps/backend/src/services/oauth/providers/discord-oauth-provider.ts @@ -18,13 +18,16 @@ export const createDiscordOAuthProvider = (config: OAuthProviderConfig): OAuthPr makeTokenExchangeRequest(config, code, Redacted.value(config.clientSecret)), getAccountInfo: (accessToken: string) => - Discord.DiscordApiClient.getAccountInfo(accessToken).pipe( + Effect.gen(function* () { + const discordApiClient = yield* Discord.DiscordApiClient + return yield* discordApiClient.getAccountInfo(accessToken) + }).pipe( Effect.provide(Discord.DiscordApiClient.layer), Effect.mapError( (error) => new AccountInfoError({ provider: "discord", - message: `Failed to get Discord account info: ${"message" in error ? String(error.message) : String(error)}`, + message: `Failed to get Discord account info: ${error instanceof Error ? error.message : String(error)}`, cause: error, }), ), From 7f5e4cc0d1df33609e3cf9b93dcd0eb67dcf9e9d Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 14:31:34 +0100 Subject: [PATCH 21/34] fix --- .../components/channel-settings/add-github-repo-modal.tsx | 2 +- .../integrations/add-github-subscription-modal.tsx | 2 +- apps/web/src/components/integrations/github-pr-embed.tsx | 2 +- apps/web/src/components/integrations/linear-issue-embed.tsx | 2 +- apps/web/src/components/link-preview.tsx | 2 +- apps/web/src/components/tweet-embed.tsx | 2 +- .../routes/_app/$orgSlug/my-settings/linked-accounts.tsx | 4 ++-- .../_app/$orgSlug/settings/integrations/$integrationId.tsx | 6 +++--- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/channel-settings/add-github-repo-modal.tsx b/apps/web/src/components/channel-settings/add-github-repo-modal.tsx index 246f0cec1..4091cad2b 100644 --- a/apps/web/src/components/channel-settings/add-github-repo-modal.tsx +++ b/apps/web/src/components/channel-settings/add-github-repo-modal.tsx @@ -62,7 +62,7 @@ export function AddGitHubRepoModal({ const repositoriesResult = useAtomValue( HazelApiClient.query("integration-resources", "getGitHubRepositories", { params: { orgId: organizationId }, - urlParams: { page: 1, perPage: 100 }, + query: { page: 1, perPage: 100 }, }), ) diff --git a/apps/web/src/components/integrations/add-github-subscription-modal.tsx b/apps/web/src/components/integrations/add-github-subscription-modal.tsx index 7f1074354..21ae421da 100644 --- a/apps/web/src/components/integrations/add-github-subscription-modal.tsx +++ b/apps/web/src/components/integrations/add-github-subscription-modal.tsx @@ -77,7 +77,7 @@ export function AddGitHubSubscriptionModal({ const repositoriesResult = useAtomValue( HazelApiClient.query("integration-resources", "getGitHubRepositories", { params: { orgId: organizationId }, - urlParams: { page: 1, perPage: 100 }, + query: { page: 1, perPage: 100 }, }), ) diff --git a/apps/web/src/components/integrations/github-pr-embed.tsx b/apps/web/src/components/integrations/github-pr-embed.tsx index 294dde1e1..901503fc6 100644 --- a/apps/web/src/components/integrations/github-pr-embed.tsx +++ b/apps/web/src/components/integrations/github-pr-embed.tsx @@ -222,7 +222,7 @@ export function GitHubPREmbed({ url, orgId }: GitHubPREmbedProps) { const resourceResult = useAtomValue( HazelApiClient.query("integration-resources", "fetchGitHubPR", { params: { orgId }, - urlParams: { url }, + query: { url }, timeToLive: "3 minutes", }), ) diff --git a/apps/web/src/components/integrations/linear-issue-embed.tsx b/apps/web/src/components/integrations/linear-issue-embed.tsx index 9b95fe618..75a012321 100644 --- a/apps/web/src/components/integrations/linear-issue-embed.tsx +++ b/apps/web/src/components/integrations/linear-issue-embed.tsx @@ -118,7 +118,7 @@ export function LinearIssueEmbed({ url, orgId }: LinearIssueEmbedProps) { const resourceResult = useAtomValue( HazelApiClient.query("integration-resources", "fetchLinearIssue", { params: { orgId }, - urlParams: { url }, + query: { url }, timeToLive: "3 minutes", }), ) diff --git a/apps/web/src/components/link-preview.tsx b/apps/web/src/components/link-preview.tsx index ac79901b7..f6a2d616b 100644 --- a/apps/web/src/components/link-preview.tsx +++ b/apps/web/src/components/link-preview.tsx @@ -6,7 +6,7 @@ import { useMemo } from "react" import { LinkPreviewClient } from "~/lib/services/common/link-preview-client" export function LinkPreview({ url }: { url: string }) { - const previewResult = useAtomValue(LinkPreviewClient.query("linkPreview", "get", { urlParams: { url } })) + const previewResult = useAtomValue(LinkPreviewClient.query("linkPreview", "get", { query: { url } })) const og = AsyncResult.getOrElse(previewResult, () => null) const isLoading = AsyncResult.isInitial(previewResult) diff --git a/apps/web/src/components/tweet-embed.tsx b/apps/web/src/components/tweet-embed.tsx index d53e76360..14981c5b7 100644 --- a/apps/web/src/components/tweet-embed.tsx +++ b/apps/web/src/components/tweet-embed.tsx @@ -237,7 +237,7 @@ interface TweetEmbedProps { } export function TweetEmbed({ id, author, messageCreatedAt }: TweetEmbedProps) { - const tweetResult = useAtomValue(LinkPreviewClient.query("tweet", "get", { urlParams: { id } })) + const tweetResult = useAtomValue(LinkPreviewClient.query("tweet", "get", { query: { id } })) const tweet = AsyncResult.getOrElse(tweetResult, () => null) const isLoading = AsyncResult.isInitial(tweetResult) diff --git a/apps/web/src/routes/_app/$orgSlug/my-settings/linked-accounts.tsx b/apps/web/src/routes/_app/$orgSlug/my-settings/linked-accounts.tsx index 76690e87b..6e3f6cee4 100644 --- a/apps/web/src/routes/_app/$orgSlug/my-settings/linked-accounts.tsx +++ b/apps/web/src/routes/_app/$orgSlug/my-settings/linked-accounts.tsx @@ -69,7 +69,7 @@ function LinkedAccountsSettings() { setIsConnectingDiscord(true) const result = await getOAuthUrlMutation({ params: { orgId: user.organizationId, provider: "discord" }, - urlParams: { level: "user" }, + query: { level: "user" }, }) if (Exit.isSuccess(result)) { @@ -86,7 +86,7 @@ function LinkedAccountsSettings() { setIsDisconnectingDiscord(true) const result = await disconnectMutation({ params: { orgId: user.organizationId, provider: "discord" }, - urlParams: { level: "user" }, + query: { level: "user" }, }) if (Exit.isSuccess(result)) { diff --git a/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx b/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx index b136d2858..d02cd76a6 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx @@ -158,7 +158,7 @@ function IntegrationConfigPage() { // This ensures the bearer token auth is properly sent const exit = await getOAuthUrlMutation({ params: { orgId: organizationId, provider: integrationId as IntegrationProvider }, - urlParams: { level: "organization" }, + query: { level: "organization" }, }) if (Exit.isSuccess(exit)) { @@ -177,7 +177,7 @@ function IntegrationConfigPage() { setIsDisconnecting(true) const exit = await disconnectMutation({ params: { orgId: organizationId, provider: integrationId as IntegrationProvider }, - urlParams: { level: "organization" }, + query: { level: "organization" }, }) exitToast(exit) @@ -764,7 +764,7 @@ function GitHubRepositoryAccessSection({ organizationId }: { organizationId: Org const repositoriesResult = useAtomValue( HazelApiClient.query("integration-resources", "getGitHubRepositories", { params: { orgId: organizationId }, - urlParams: { page, perPage }, + query: { page, perPage }, }), ) From 967176871ba8c924d250861eccdb81905ed9490e Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 14:38:50 +0100 Subject: [PATCH 22/34] stuff --- apps/backend/scripts/create-bot.ts | 4 ++-- .../backend/scripts/rebuild-channel-access.ts | 16 +++++++++----- apps/backend/scripts/reset-all.ts | 4 ++-- apps/backend/scripts/seed-internal-bots.ts | 4 ++-- apps/backend/scripts/setup.ts | 4 ++-- apps/backend/src/lib/policy-utils.test.ts | 6 ++--- apps/backend/src/policies/bot-policy.test.ts | 2 +- .../policies/channel-member-policy.test.ts | 10 ++++----- .../src/policies/channel-policy.test.ts | 2 +- .../integration-connection-policy.test.ts | 2 +- .../src/policies/message-policy.test.ts | 2 +- .../policies/message-reaction-policy.test.ts | 2 +- .../organization-member-policy.test.ts | 4 ++-- .../src/policies/organization-policy.test.ts | 4 ++-- .../policies/pinned-message-policy.test.ts | 2 +- .../policies/typing-indicator-policy.test.ts | 2 +- apps/backend/src/routes/auth.http.test.ts | 21 ++++++++---------- .../src/routes/integrations.http.test.ts | 2 +- apps/backend/src/routes/internal.http.ts | 4 ++-- .../src/rpc/handlers/connect-shares.test.ts | 10 ++++----- apps/backend/src/scripts/sync-workos.ts | 4 ++-- .../src/services/bot-gateway-service.test.ts | 15 +++++++------ .../chat-sync/chat-sync-core-worker.ts | 7 ++++-- .../chat-sync/discord-sync-worker.test.ts | 2 +- .../services/chat-sync/discord-sync-worker.ts | 7 ++++-- .../src/services/integration-token-service.ts | 22 ++++++++++--------- .../message-outbox-dispatcher.test.ts | 11 +++++----- .../src/services/message-outbox-dispatcher.ts | 2 +- .../message-side-effect-service.test.ts | 13 +++++------ .../services/oauth/oauth-provider-registry.ts | 20 +++++++++-------- .../backend/src/services/org-resolver.test.ts | 4 ++-- apps/backend/src/services/rate-limiter.ts | 2 +- apps/backend/src/test/effect-helpers.ts | 21 ++++++++++++++++++ .../src/components/chat/message-content.tsx | 5 +++-- apps/web/src/components/chat/message-item.tsx | 5 +++-- apps/web/src/components/chat/message-list.tsx | 7 +++--- apps/web/src/components/chat/message.tsx | 8 +++---- .../components/chat/pinned-messages-modal.tsx | 11 +++++----- .../components/chat/thread-message-list.tsx | 5 +++-- .../components/status/user-status-badge.tsx | 3 ++- apps/web/src/lib/utils.ts | 17 ++++++++++++++ apps/web/src/utils/status.ts | 4 ++-- 42 files changed, 179 insertions(+), 123 deletions(-) create mode 100644 apps/backend/src/test/effect-helpers.ts diff --git a/apps/backend/scripts/create-bot.ts b/apps/backend/scripts/create-bot.ts index 49100f362..a9d824ff0 100644 --- a/apps/backend/scripts/create-bot.ts +++ b/apps/backend/scripts/create-bot.ts @@ -15,7 +15,7 @@ import { Database, schema } from "@hazel/db" import type { BotId, BotInstallationId, OrganizationId, OrganizationMemberId, UserId } from "@hazel/schema" -import { Effect, Logger, LogLevel } from "effect" +import { Effect, References } from "effect" import { randomUUID } from "crypto" import { DatabaseLive } from "../src/services/database" @@ -138,7 +138,7 @@ const createBotScript = Effect.gen(function* () { // Run the script with proper Effect runtime const runnable = createBotScript.pipe( Effect.provide(DatabaseLive), - Effect.provide(Layer.succeed(References.MinimumLogLevel, LogLevel.Info)), + Effect.provideService(References.MinimumLogLevel, "Info"), ) Effect.runPromise(runnable).catch((error) => { diff --git a/apps/backend/scripts/rebuild-channel-access.ts b/apps/backend/scripts/rebuild-channel-access.ts index ca730f7c4..39a49092f 100644 --- a/apps/backend/scripts/rebuild-channel-access.ts +++ b/apps/backend/scripts/rebuild-channel-access.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun import { and, Database, eq, isNull, ne, schema } from "@hazel/db" -import { Effect, Layer, Logger, LogLevel } from "effect" +import { Effect, Layer, References } from "effect" import { ChannelAccessSyncService } from "../src/services/channel-access-sync" import { DatabaseLive } from "../src/services/database" @@ -21,7 +21,7 @@ const rebuildChannelAccess = Effect.gen(function* () { yield* Effect.forEach( activeNonThreadChannels, - (channel) => ChannelAccessSyncService.syncChannel(channel.id), + (channel) => ChannelAccessSyncService.use((service) => service.syncChannel(channel.id)), { concurrency: 20 }, ) @@ -32,9 +32,13 @@ const rebuildChannelAccess = Effect.gen(function* () { .where(and(isNull(schema.channelsTable.deletedAt), eq(schema.channelsTable.type, "thread"))), ) - yield* Effect.forEach(threadChannels, (channel) => ChannelAccessSyncService.syncChannel(channel.id), { - concurrency: 20, - }) + yield* Effect.forEach( + threadChannels, + (channel) => ChannelAccessSyncService.use((service) => service.syncChannel(channel.id)), + { + concurrency: 20, + }, + ) const countResult = yield* db.execute( (client) => client.$client`SELECT COUNT(*)::int AS count FROM channel_access`, @@ -52,7 +56,7 @@ Effect.runPromise( rebuildChannelAccess.pipe( Effect.provide(ChannelAccessSyncLive), Effect.provide(DatabaseLive), - Effect.provide(Layer.succeed(References.MinimumLogLevel, LogLevel.Info)), + Effect.provideService(References.MinimumLogLevel, "Info"), ), ).catch((error) => { console.error("Failed to rebuild channel_access", error) diff --git a/apps/backend/scripts/reset-all.ts b/apps/backend/scripts/reset-all.ts index 253209f09..c47c813fb 100644 --- a/apps/backend/scripts/reset-all.ts +++ b/apps/backend/scripts/reset-all.ts @@ -2,7 +2,7 @@ import { Database } from "@hazel/db" import { WorkOSClient } from "@hazel/backend-core" -import { Effect, Logger, LogLevel } from "effect" +import { Effect, References } from "effect" import { DatabaseLive } from "../src/services/database" // Parse command line arguments @@ -258,7 +258,7 @@ const resetScript = Effect.gen(function* () { const runnable = resetScript.pipe( Effect.provide(DatabaseLive), Effect.provide(WorkOSClient.layer), - Effect.provide(Layer.succeed(References.MinimumLogLevel, LogLevel.Info)), + Effect.provideService(References.MinimumLogLevel, "Info"), ) Effect.runPromise(runnable).catch((error) => { diff --git a/apps/backend/scripts/seed-internal-bots.ts b/apps/backend/scripts/seed-internal-bots.ts index d515105cd..8e4887041 100644 --- a/apps/backend/scripts/seed-internal-bots.ts +++ b/apps/backend/scripts/seed-internal-bots.ts @@ -15,7 +15,7 @@ import { Database, schema } from "@hazel/db" import type { BotId, UserId } from "@hazel/schema" import { createHash, randomUUID } from "crypto" import { eq } from "drizzle-orm" -import { Effect, Logger, LogLevel } from "effect" +import { Effect, References } from "effect" import { DatabaseLive } from "../src/services/database" // Fixed namespace for deterministic UUID generation (DNS namespace UUID) @@ -217,7 +217,7 @@ const seedInternalBots = Effect.gen(function* () { // Run the script const runnable = seedInternalBots.pipe( Effect.provide(DatabaseLive), - Effect.provide(Layer.succeed(References.MinimumLogLevel, LogLevel.Info)), + Effect.provideService(References.MinimumLogLevel, "Info"), ) Effect.runPromise(runnable).catch((error) => { diff --git a/apps/backend/scripts/setup.ts b/apps/backend/scripts/setup.ts index 92fa8acc6..098ebf24a 100644 --- a/apps/backend/scripts/setup.ts +++ b/apps/backend/scripts/setup.ts @@ -9,7 +9,7 @@ import { WorkOSClient, WorkOSSync, } from "@hazel/backend-core" -import { Effect, Layer, Logger, LogLevel } from "effect" +import { Effect, Layer, References } from "effect" import { DatabaseLive } from "../src/services/database" // ANSI color codes @@ -123,7 +123,7 @@ const MainLive = Layer.mergeAll(WorkOSSync.layer, WorkOSClient.layer).pipe( const runnable = setupScript.pipe( Effect.provide(MainLive), - Effect.provide(Layer.succeed(References.MinimumLogLevel, LogLevel.Info)), + Effect.provideService(References.MinimumLogLevel, "Info"), ) Effect.runPromise(runnable).catch((error) => { diff --git a/apps/backend/src/lib/policy-utils.test.ts b/apps/backend/src/lib/policy-utils.test.ts index 0cc568df2..0e8f9c79b 100644 --- a/apps/backend/src/lib/policy-utils.test.ts +++ b/apps/backend/src/lib/policy-utils.test.ts @@ -30,7 +30,7 @@ describe("policy-utils", () => { expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(UnauthorizedError.is(result.left)).toBe(true) + expect(UnauthorizedError.is(result.failure)).toBe(true) } }) @@ -45,7 +45,7 @@ describe("policy-utils", () => { expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(UnauthorizedError.is(result.left)).toBe(true) + expect(UnauthorizedError.is(result.failure)).toBe(true) } }) }) @@ -66,7 +66,7 @@ describe("policy-utils", () => { expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(result.left).toBe(existing) + expect(result.failure).toBe(existing) } }) }) diff --git a/apps/backend/src/policies/bot-policy.test.ts b/apps/backend/src/policies/bot-policy.test.ts index 1775ed29a..d6d7f8343 100644 --- a/apps/backend/src/policies/bot-policy.test.ts +++ b/apps/backend/src/policies/bot-policy.test.ts @@ -100,7 +100,7 @@ describe("BotPolicy", () => { expect(Result.isFailure(updateOther)).toBe(true) expect(Result.isFailure(deleteMissing)).toBe(true) if (Result.isFailure(deleteMissing)) { - expect(UnauthorizedError.is(deleteMissing.left)).toBe(true) + expect(UnauthorizedError.is(deleteMissing.failure)).toBe(true) } }) diff --git a/apps/backend/src/policies/channel-member-policy.test.ts b/apps/backend/src/policies/channel-member-policy.test.ts index 52be7cbaf..b11becd48 100644 --- a/apps/backend/src/policies/channel-member-policy.test.ts +++ b/apps/backend/src/policies/channel-member-policy.test.ts @@ -105,7 +105,7 @@ describe("ChannelMemberPolicy", () => { const result = await runWithActorEither(ChannelMemberPolicy.isOwner(CHANNEL_MEMBER_ID), layer, actor) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(UnauthorizedError.is(result.left)).toBe(true) + expect(UnauthorizedError.is(result.failure)).toBe(true) } }) @@ -159,7 +159,7 @@ describe("ChannelMemberPolicy", () => { const result = await runWithActorEither(ChannelMemberPolicy.canCreate(CHANNEL_ID), layer, actor) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(UnauthorizedError.is(result.left)).toBe(true) + expect(UnauthorizedError.is(result.failure)).toBe(true) } }) @@ -212,7 +212,7 @@ describe("ChannelMemberPolicy", () => { const result = await runWithActorEither(ChannelMemberPolicy.canRead(CHANNEL_ID), layer, actor) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(UnauthorizedError.is(result.left)).toBe(true) + expect(UnauthorizedError.is(result.failure)).toBe(true) } }) @@ -272,7 +272,7 @@ describe("ChannelMemberPolicy", () => { // Owner is NOT allowed (canUpdate checks role === "admin", not isAdminOrOwner) expect(Result.isFailure(ownerResult)).toBe(true) if (Result.isFailure(ownerResult)) { - expect(UnauthorizedError.is(ownerResult.left)).toBe(true) + expect(UnauthorizedError.is(ownerResult.failure)).toBe(true) } }) @@ -332,7 +332,7 @@ describe("ChannelMemberPolicy", () => { // Owner is NOT allowed (canDelete checks role === "admin", not isAdminOrOwner) expect(Result.isFailure(ownerResult)).toBe(true) if (Result.isFailure(ownerResult)) { - expect(UnauthorizedError.is(ownerResult.left)).toBe(true) + expect(UnauthorizedError.is(ownerResult.failure)).toBe(true) } }) }) diff --git a/apps/backend/src/policies/channel-policy.test.ts b/apps/backend/src/policies/channel-policy.test.ts index d59c6e9a9..caa2c67f1 100644 --- a/apps/backend/src/policies/channel-policy.test.ts +++ b/apps/backend/src/policies/channel-policy.test.ts @@ -97,7 +97,7 @@ describe("ChannelPolicy", () => { expect(Result.isSuccess(allowed)).toBe(true) expect(Result.isFailure(missing)).toBe(true) if (Result.isFailure(missing)) { - expect(UnauthorizedError.is(missing.left)).toBe(true) + expect(UnauthorizedError.is(missing.failure)).toBe(true) } }) diff --git a/apps/backend/src/policies/integration-connection-policy.test.ts b/apps/backend/src/policies/integration-connection-policy.test.ts index 1394b823d..6b8162d20 100644 --- a/apps/backend/src/policies/integration-connection-policy.test.ts +++ b/apps/backend/src/policies/integration-connection-policy.test.ts @@ -47,7 +47,7 @@ describe("IntegrationConnectionPolicy", () => { expect(Result.isFailure(del)).toBe(true) if (Result.isFailure(insert)) { - expect(UnauthorizedError.is(insert.left)).toBe(true) + expect(UnauthorizedError.is(insert.failure)).toBe(true) } }) }) diff --git a/apps/backend/src/policies/message-policy.test.ts b/apps/backend/src/policies/message-policy.test.ts index ff04387bc..89b16dd56 100644 --- a/apps/backend/src/policies/message-policy.test.ts +++ b/apps/backend/src/policies/message-policy.test.ts @@ -260,7 +260,7 @@ describe("MessagePolicy", () => { const result = await runWithActorEither(MessagePolicy.canDelete(MISSING_MESSAGE_ID), layer, actor) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(UnauthorizedError.is(result.left)).toBe(true) + expect(UnauthorizedError.is(result.failure)).toBe(true) } }) }) diff --git a/apps/backend/src/policies/message-reaction-policy.test.ts b/apps/backend/src/policies/message-reaction-policy.test.ts index b84ec7f2a..0d4793ee6 100644 --- a/apps/backend/src/policies/message-reaction-policy.test.ts +++ b/apps/backend/src/policies/message-reaction-policy.test.ts @@ -174,7 +174,7 @@ describe("MessageReactionPolicy", () => { const result = await runWithActorEither(MessageReactionPolicy.canDelete(REACTION_ID), layer, actor) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(UnauthorizedError.is(result.left)).toBe(true) + expect(UnauthorizedError.is(result.failure)).toBe(true) } }) }) diff --git a/apps/backend/src/policies/organization-member-policy.test.ts b/apps/backend/src/policies/organization-member-policy.test.ts index 0f483be0e..1110d0329 100644 --- a/apps/backend/src/policies/organization-member-policy.test.ts +++ b/apps/backend/src/policies/organization-member-policy.test.ts @@ -89,7 +89,7 @@ describe("OrganizationMemberPolicy", () => { const result = await runWithActorEither(OrganizationMemberPolicy.canUpdate(MEMBER_ID), layer, owner) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(UnauthorizedError.is(result.left)).toBe(true) + expect(UnauthorizedError.is(result.failure)).toBe(true) } }) @@ -140,7 +140,7 @@ describe("OrganizationMemberPolicy", () => { const result = await runWithActorEither(OrganizationMemberPolicy.canDelete(MEMBER_ID), layer, owner) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(UnauthorizedError.is(result.left)).toBe(true) + expect(UnauthorizedError.is(result.failure)).toBe(true) } }) }) diff --git a/apps/backend/src/policies/organization-policy.test.ts b/apps/backend/src/policies/organization-policy.test.ts index 77b29797f..f65864480 100644 --- a/apps/backend/src/policies/organization-policy.test.ts +++ b/apps/backend/src/policies/organization-policy.test.ts @@ -51,7 +51,7 @@ describe("OrganizationPolicy", () => { expect(Result.isSuccess(adminResult)).toBe(true) expect(Result.isFailure(memberResult)).toBe(true) if (Result.isFailure(memberResult)) { - expect(UnauthorizedError.is(memberResult.left)).toBe(true) + expect(UnauthorizedError.is(memberResult.failure)).toBe(true) } }) @@ -90,7 +90,7 @@ describe("OrganizationPolicy", () => { const result = await runWithActorEither(OrganizationPolicy.isMember(TEST_ORG_ID), layer, actor) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(UnauthorizedError.is(result.left)).toBe(true) + expect(UnauthorizedError.is(result.failure)).toBe(true) } }) diff --git a/apps/backend/src/policies/pinned-message-policy.test.ts b/apps/backend/src/policies/pinned-message-policy.test.ts index f2b75c981..77d5fa067 100644 --- a/apps/backend/src/policies/pinned-message-policy.test.ts +++ b/apps/backend/src/policies/pinned-message-policy.test.ts @@ -180,7 +180,7 @@ describe("PinnedMessagePolicy", () => { const result = await runWithActorEither(PinnedMessagePolicy.canDelete(PINNED_MSG_ID), layer, outsider) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(UnauthorizedError.is(result.left)).toBe(true) + expect(UnauthorizedError.is(result.failure)).toBe(true) } }) }) diff --git a/apps/backend/src/policies/typing-indicator-policy.test.ts b/apps/backend/src/policies/typing-indicator-policy.test.ts index 049f4696c..e14ffc835 100644 --- a/apps/backend/src/policies/typing-indicator-policy.test.ts +++ b/apps/backend/src/policies/typing-indicator-policy.test.ts @@ -135,7 +135,7 @@ describe("TypingIndicatorPolicy", () => { expect(Result.isFailure(otherDenied)).toBe(true) expect(Result.isFailure(missingDenied)).toBe(true) if (Result.isFailure(missingDenied)) { - expect(UnauthorizedError.is(missingDenied.left)).toBe(true) + expect(UnauthorizedError.is(missingDenied.failure)).toBe(true) } }) diff --git a/apps/backend/src/routes/auth.http.test.ts b/apps/backend/src/routes/auth.http.test.ts index e9ad1de29..dd3f77b56 100644 --- a/apps/backend/src/routes/auth.http.test.ts +++ b/apps/backend/src/routes/auth.http.test.ts @@ -1,22 +1,19 @@ import { describe, expect, it, layer } from "@effect/vitest" import { OrganizationMemberRepo, UserRepo } from "@hazel/backend-core" import type { OrganizationId, UserId } from "@hazel/schema" -import { ConfigProvider, Effect, Layer, Option, Schema } from "effect" +import { Effect, Layer, Option, Schema, ServiceMap } from "effect" import { AuthState, RelativeUrl } from "../lib/schema.ts" +import { configLayer } from "../test/effect-helpers" import { WorkOSAuth as WorkOS, WorkOSAuthError as WorkOSApiError } from "../services/workos-auth.ts" // ===== Mock Configuration ===== -const TestConfigProvider = ConfigProvider.fromMap( - new Map([ - ["WORKOS_CLIENT_ID", "client_test_123"], - ["WORKOS_REDIRECT_URI", "http://localhost:3000/auth/callback"], - ["FRONTEND_URL", "http://localhost:3000"], - ["WORKOS_API_KEY", "sk_test_123"], - ]), -) - -const TestConfigLive = Layer.setConfigProvider(TestConfigProvider) +const TestConfigLive = configLayer({ + WORKOS_CLIENT_ID: "client_test_123", + WORKOS_REDIRECT_URI: "http://localhost:3000/auth/callback", + FRONTEND_URL: "http://localhost:3000", + WORKOS_API_KEY: "sk_test_123", +}) // ===== Mock WorkOS Service ===== @@ -99,7 +96,7 @@ const createMockWorkOSLive = (options?: { }, catch: (cause) => new WorkOSApiError({ cause }), }), - } as unknown as WorkOS) + } as ServiceMap.Service.Shape) // ===== Mock UserRepo ===== diff --git a/apps/backend/src/routes/integrations.http.test.ts b/apps/backend/src/routes/integrations.http.test.ts index 47f459356..021ec3c7a 100644 --- a/apps/backend/src/routes/integrations.http.test.ts +++ b/apps/backend/src/routes/integrations.http.test.ts @@ -11,7 +11,7 @@ const expectInvalidBaseUrl = async (url: string) => { const result = await Effect.runPromise(validateCraftBaseUrl(url).pipe(Effect.result)) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(result.left._tag).toBe("InvalidApiKeyError") + expect(result.failure._tag).toBe("InvalidApiKeyError") } } diff --git a/apps/backend/src/routes/internal.http.ts b/apps/backend/src/routes/internal.http.ts index a66175dd2..ba10878ca 100644 --- a/apps/backend/src/routes/internal.http.ts +++ b/apps/backend/src/routes/internal.http.ts @@ -22,8 +22,8 @@ export const HttpInternalLive = HttpApiBuilder.group(HazelApi, "internal", (hand const request = yield* HttpServerRequest.HttpServerRequest // Optionally verify internal secret for server-to-server auth - const internalSecretOption = yield* Effect.config( - Config.string("INTERNAL_SECRET").pipe(Config.option), + const internalSecretOption = yield* Effect.orDie( + Config.string("INTERNAL_SECRET").pipe(Config.option).asEffect(), ) const internalSecret = Option.getOrUndefined(internalSecretOption) diff --git a/apps/backend/src/rpc/handlers/connect-shares.test.ts b/apps/backend/src/rpc/handlers/connect-shares.test.ts index ff0796645..edec41d14 100644 --- a/apps/backend/src/rpc/handlers/connect-shares.test.ts +++ b/apps/backend/src/rpc/handlers/connect-shares.test.ts @@ -28,7 +28,7 @@ describe("connect-shares helpers", () => { expect(result._tag).toBe("Failure") if (result._tag === "Failure") { - expect(result.left).toBeInstanceOf(ConnectWorkspaceNotFoundError) + expect(result.failure).toBeInstanceOf(ConnectWorkspaceNotFoundError) } }) @@ -47,7 +47,7 @@ describe("connect-shares helpers", () => { expect(result._tag).toBe("Failure") if (result._tag === "Failure") { - expect(result.left).toBeInstanceOf(ConnectWorkspaceNotFoundError) + expect(result.failure).toBeInstanceOf(ConnectWorkspaceNotFoundError) } }) @@ -66,7 +66,7 @@ describe("connect-shares helpers", () => { expect(result._tag).toBe("Failure") if (result._tag === "Failure") { - expect(result.left).toBeInstanceOf(ConnectWorkspaceNotFoundError) + expect(result.failure).toBeInstanceOf(ConnectWorkspaceNotFoundError) } }) @@ -80,7 +80,7 @@ describe("connect-shares helpers", () => { expect(result._tag).toBe("Failure") if (result._tag === "Failure") { - expect(result.left).toBeInstanceOf(PermissionError) + expect(result.failure).toBeInstanceOf(PermissionError) } }) @@ -116,7 +116,7 @@ describe("connect-shares helpers", () => { expect(result._tag).toBe("Failure") if (result._tag === "Failure") { - expect(result.left).toBeInstanceOf(ConnectChannelAlreadySharedError) + expect(result.failure).toBeInstanceOf(ConnectChannelAlreadySharedError) } }) }) diff --git a/apps/backend/src/scripts/sync-workos.ts b/apps/backend/src/scripts/sync-workos.ts index c50f358c1..76018f6c8 100644 --- a/apps/backend/src/scripts/sync-workos.ts +++ b/apps/backend/src/scripts/sync-workos.ts @@ -25,6 +25,6 @@ const syncWorkos = Effect.gen(function* () { const workOsSync = yield* WorkOSSync yield* workOsSync.syncAll -}).pipe(Effect.provide(MainLive), Effect.provide(Logger.prettyLayer())) +}).pipe(Effect.provide(MainLive), Effect.provide(Logger.layer([Logger.consolePretty()]))) -Effect.runPromise(syncWorkos) +Effect.runPromise(syncWorkos as Effect.Effect) diff --git a/apps/backend/src/services/bot-gateway-service.test.ts b/apps/backend/src/services/bot-gateway-service.test.ts index f2a42f8d1..fd12431f9 100644 --- a/apps/backend/src/services/bot-gateway-service.test.ts +++ b/apps/backend/src/services/bot-gateway-service.test.ts @@ -2,7 +2,8 @@ import { describe, expect, it } from "@effect/vitest" import { BotInstallationRepo, ChannelRepo } from "@hazel/backend-core" import { createBotGatewayPartitionKey } from "@hazel/domain" import type { BotId, ChannelId, OrganizationId, UserId } from "@hazel/schema" -import { ConfigProvider, Effect, Layer, Option } from "effect" +import { Effect, Layer, Option, ServiceMap } from "effect" +import { buildServiceLayer, configLayer } from "../test/effect-helpers" import { BotGatewayService } from "./bot-gateway-service" const DURABLE_STREAMS_URL = "http://durable.test/v1/stream" @@ -12,14 +13,14 @@ const CHANNEL_ID = "00000000-0000-0000-0000-000000000444" as ChannelId const ORG_ID = "00000000-0000-0000-0000-000000000333" as OrganizationId const USER_ID = "00000000-0000-0000-0000-000000000222" as UserId -const TestConfigLive = Layer.setConfigProvider( - ConfigProvider.fromMap(new Map([["DURABLE_STREAMS_URL", DURABLE_STREAMS_URL]])), -) +const TestConfigLive = configLayer({ + DURABLE_STREAMS_URL, +}) const makeBotInstallationRepoLayer = (botIds: ReadonlyArray) => Layer.succeed(BotInstallationRepo, { getBotIdsForOrg: () => Effect.succeed([...botIds]), - } as unknown as BotInstallationRepo) + } as ServiceMap.Service.Shape) const makeChannelRepoLayer = (organizationId: OrganizationId) => Layer.succeed(ChannelRepo, { @@ -30,10 +31,10 @@ const makeChannelRepoLayer = (organizationId: OrganizationId) => organizationId, }), ), - } as unknown as ChannelRepo) + } as ServiceMap.Service.Shape) const makeServiceLayer = (botIds: ReadonlyArray) => - BotGatewayService.DefaultWithoutDependencies.pipe( + buildServiceLayer(BotGatewayService).pipe( Layer.provide(makeBotInstallationRepoLayer(botIds)), Layer.provide(makeChannelRepoLayer(ORG_ID)), Layer.provide(TestConfigLive), diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index 8a2d8591d..ac14a612d 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -151,8 +151,6 @@ export interface ChatSyncIngressThreadCreate { readonly dedupeKey?: string } -export class ChatSyncCoreWorker extends ServiceMap.Service()("ChatSyncCoreWorker") {} - /** @internal — exported for integration tests that provide their own deps */ export const ChatSyncCoreWorkerMake = Effect.gen(function* () { const db = yield* Database.Database @@ -2737,6 +2735,11 @@ export const ChatSyncCoreWorkerMake = Effect.gen(function* () { } }) +export class ChatSyncCoreWorker extends ServiceMap.Service< + ChatSyncCoreWorker, + Effect.Effect.Success +>()("ChatSyncCoreWorker") {} + export const ChatSyncCoreWorkerLayer = Layer.effect(ChatSyncCoreWorker, ChatSyncCoreWorkerMake).pipe( Layer.provide(ChatSyncConnectionRepo.layer), Layer.provide(ChatSyncChannelLinkRepo.layer), diff --git a/apps/backend/src/services/chat-sync/discord-sync-worker.test.ts b/apps/backend/src/services/chat-sync/discord-sync-worker.test.ts index 445311447..477fccfaa 100644 --- a/apps/backend/src/services/chat-sync/discord-sync-worker.test.ts +++ b/apps/backend/src/services/chat-sync/discord-sync-worker.test.ts @@ -3195,7 +3195,7 @@ describe("DiscordSyncWorker outbound attachments primitive", () => { expect(result._tag).toBe("Failure") if (result._tag === "Failure") { - expect((result.left as { _tag?: string })._tag).toBe("DiscordSyncConfigurationError") + expect((result.failure as { _tag?: string })._tag).toBe("DiscordSyncConfigurationError") } } finally { if (originalS3PublicUrl === undefined) { diff --git a/apps/backend/src/services/chat-sync/discord-sync-worker.ts b/apps/backend/src/services/chat-sync/discord-sync-worker.ts index c6b9c64ad..47b5d1c91 100644 --- a/apps/backend/src/services/chat-sync/discord-sync-worker.ts +++ b/apps/backend/src/services/chat-sync/discord-sync-worker.ts @@ -32,8 +32,6 @@ export { DiscordSyncMessageNotFoundError, } -export class DiscordSyncWorker extends ServiceMap.Service()("DiscordSyncWorker") {} - const DiscordSyncWorkerMake = Effect.gen(function* () { const coreWorker = yield* ChatSyncCoreWorker @@ -223,6 +221,11 @@ const DiscordSyncWorkerMake = Effect.gen(function* () { } }) +export class DiscordSyncWorker extends ServiceMap.Service< + DiscordSyncWorker, + Effect.Effect.Success +>()("DiscordSyncWorker") {} + export const DiscordSyncWorkerLayer = Layer.effect(DiscordSyncWorker, DiscordSyncWorkerMake).pipe( Layer.provide(ChatSyncCoreWorkerLayer), ) diff --git a/apps/backend/src/services/integration-token-service.ts b/apps/backend/src/services/integration-token-service.ts index 20dc9947b..9881a57ee 100644 --- a/apps/backend/src/services/integration-token-service.ts +++ b/apps/backend/src/services/integration-token-service.ts @@ -223,17 +223,19 @@ export class IntegrationTokenService extends ServiceMap.Service - new TokenRefreshError({ - provider: connection.provider, - cause: `Failed to load provider config: ${cause}`, - }), - ), + const providerConfig = yield* loadProviderConfig( + connection.provider as OAuthIntegrationProvider, ) + .asEffect() + .pipe( + Effect.mapError( + (cause) => + new TokenRefreshError({ + provider: connection.provider, + cause: `Failed to load provider config: ${cause}`, + }), + ), + ) yield* Effect.logDebug("Refreshing OAuth token", { provider: connection.provider }) diff --git a/apps/backend/src/services/message-outbox-dispatcher.test.ts b/apps/backend/src/services/message-outbox-dispatcher.test.ts index ba76cae04..0841ed6a8 100644 --- a/apps/backend/src/services/message-outbox-dispatcher.test.ts +++ b/apps/backend/src/services/message-outbox-dispatcher.test.ts @@ -9,10 +9,11 @@ import { } from "@hazel/backend-core" import { Database, asc, eq, inArray, schema } from "@hazel/db" import type { ChannelId, MessageId, MessageOutboxEventId, UserId } from "@hazel/schema" -import { Effect, Layer, Redacted } from "effect" +import { Effect, Layer, Redacted, ServiceMap } from "effect" import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import { EnvVars } from "../lib/env-vars" import { createChatSyncDbHarness, type ChatSyncDbHarness } from "../test/chat-sync-db-harness" +import { buildServiceLayer } from "../test/effect-helpers" import { MessageOutboxDispatcher } from "./message-outbox-dispatcher" import { MessageSideEffectService } from "./message-side-effect-service" @@ -42,15 +43,15 @@ const runDispatcherEffect = ( ) => Effect.runPromise( Effect.scoped( - effect.pipe( - Effect.provide(MessageOutboxDispatcher.DefaultWithoutDependencies), + make.pipe( + Effect.provide(buildServiceLayer(MessageOutboxDispatcher)), Effect.provide(Layer.succeed(MessageSideEffectService, sideEffects)), Effect.provide(MessageOutboxRepo.layer), Effect.provide( Layer.succeed(EnvVars, { IS_DEV: true, DATABASE_URL: Redacted.make(harness.container.getConnectionUri()), - } as EnvVars), + } as ServiceMap.Service.Shape), ), Effect.provide(harness.dbLayer), ), @@ -92,7 +93,7 @@ const makeSideEffectService = (calls: SideEffectCall[], options: SideEffectOptio Effect.sync(() => { calls.push({ eventType: "reaction_deleted", payload, dedupeKey }) }), - } as unknown as MessageSideEffectService + } as ServiceMap.Service.Shape } const waitFor = async (predicate: () => Promise, timeoutMs = 8_000) => { diff --git a/apps/backend/src/services/message-outbox-dispatcher.ts b/apps/backend/src/services/message-outbox-dispatcher.ts index 9ef3efbd3..70b26aa6d 100644 --- a/apps/backend/src/services/message-outbox-dispatcher.ts +++ b/apps/backend/src/services/message-outbox-dispatcher.ts @@ -169,7 +169,7 @@ export class MessageOutboxDispatcher extends ServiceMap.Service => + const campaignForLeadership = (): Effect.Effect => Effect.gen(function* () { const reservedResult = yield* Effect.tryPromise({ try: (): Promise => pool.connect(), diff --git a/apps/backend/src/services/message-side-effect-service.test.ts b/apps/backend/src/services/message-side-effect-service.test.ts index 0c4027cc8..02fa2f721 100644 --- a/apps/backend/src/services/message-side-effect-service.test.ts +++ b/apps/backend/src/services/message-side-effect-service.test.ts @@ -2,9 +2,10 @@ import { FetchHttpClient } from "effect/unstable/http" import { randomUUID } from "node:crypto" import { Database, schema } from "@hazel/db" import type { ChannelId, MessageId, MessageReactionId, OrganizationId, UserId } from "@hazel/schema" -import { ConfigProvider, Effect, Layer } from "effect" +import { Effect, Layer, ServiceMap } from "effect" import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import { createChatSyncDbHarness, type ChatSyncDbHarness } from "../test/chat-sync-db-harness" +import { buildServiceLayer, configLayer } from "../test/effect-helpers" import { DiscordSyncWorker } from "./chat-sync/discord-sync-worker" import { MessageSideEffectService } from "./message-side-effect-service" @@ -40,12 +41,10 @@ const runServiceEffect = ( ) => Effect.runPromise( Effect.scoped( - effect.pipe( - Effect.provide(MessageSideEffectService.DefaultWithoutDependencies), + make.pipe( + Effect.provide(buildServiceLayer(MessageSideEffectService)), Effect.provide(Layer.succeed(DiscordSyncWorker, worker)), - Effect.provide( - Layer.setConfigProvider(ConfigProvider.fromMap(new Map([["CLUSTER_URL", CLUSTER_URL]]))), - ), + Effect.provide(configLayer({ CLUSTER_URL })), Effect.provide(FetchHttpClient.layer), Effect.provide(harness.dbLayer), ), @@ -89,7 +88,7 @@ const makeDiscordWorker = (calls: DiscordCall[], options: WorkerOptions = {}) => calls.push({ method: "reaction_delete", payload, dedupeKey }) return { synced: 1, failed: 0 } }), - }) as unknown as DiscordSyncWorker + }) as ServiceMap.Service.Shape const seedMessageSideEffectState = (harness: ChatSyncDbHarness) => harness.run( diff --git a/apps/backend/src/services/oauth/oauth-provider-registry.ts b/apps/backend/src/services/oauth/oauth-provider-registry.ts index 3620f1696..d3e687943 100644 --- a/apps/backend/src/services/oauth/oauth-provider-registry.ts +++ b/apps/backend/src/services/oauth/oauth-provider-registry.ts @@ -125,15 +125,17 @@ export class OAuthProviderRegistry extends ServiceMap.Service - new ProviderNotConfiguredError({ - provider: oauthProvider_, - message: `Missing configuration for ${provider}: ${String(error)}`, - }), - ), - ) + const config = yield* loadProviderConfig(oauthProvider_) + .asEffect() + .pipe( + Effect.mapError( + (error) => + new ProviderNotConfiguredError({ + provider: oauthProvider_, + message: `Missing configuration for ${provider}: ${String(error)}`, + }), + ), + ) // Create and cache provider const oauthProvider = factory(config) diff --git a/apps/backend/src/services/org-resolver.test.ts b/apps/backend/src/services/org-resolver.test.ts index 9034a3a3c..2cadb1f1f 100644 --- a/apps/backend/src/services/org-resolver.test.ts +++ b/apps/backend/src/services/org-resolver.test.ts @@ -110,7 +110,7 @@ describe("OrgResolver", () => { ) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(PermissionError.is(result.left)).toBe(true) + expect(PermissionError.is(result.failure)).toBe(true) } }) }) @@ -163,7 +163,7 @@ describe("OrgResolver", () => { ) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { - expect(PermissionError.is(result.left)).toBe(true) + expect(PermissionError.is(result.failure)).toBe(true) } }) }) diff --git a/apps/backend/src/services/rate-limiter.ts b/apps/backend/src/services/rate-limiter.ts index 5802561c3..8fa6b5d06 100644 --- a/apps/backend/src/services/rate-limiter.ts +++ b/apps/backend/src/services/rate-limiter.ts @@ -98,7 +98,7 @@ export class RateLimiter extends ServiceMap.Service()("RateLimiter" } }), }) { - static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(Redis.layer)) + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(Redis.Default)) } /** diff --git a/apps/backend/src/test/effect-helpers.ts b/apps/backend/src/test/effect-helpers.ts new file mode 100644 index 000000000..63a08bcb6 --- /dev/null +++ b/apps/backend/src/test/effect-helpers.ts @@ -0,0 +1,21 @@ +import { ConfigProvider, Effect, Layer, ServiceMap } from "effect" + +export const buildServiceLayer = < + T extends ServiceMap.Service.Any & { + readonly make: Effect.Effect + }, +>( + service: T, +) => Layer.effect(service, service.make) + +export const serviceEffect = ( + service: T, + f: (implementation: ServiceMap.Service.Shape) => Effect.Effect, +) => service.use(f) + +export const serviceShape = ( + shape: Partial>, +) => shape as ServiceMap.Service.Shape + +export const configLayer = (values: Record) => + ConfigProvider.layer(ConfigProvider.fromUnknown(values)) diff --git a/apps/web/src/components/chat/message-content.tsx b/apps/web/src/components/chat/message-content.tsx index e13d2e4e9..ccbe2b751 100644 --- a/apps/web/src/components/chat/message-content.tsx +++ b/apps/web/src/components/chat/message-content.tsx @@ -1,5 +1,6 @@ import type { OrganizationId } from "@hazel/schema" import { createContext, lazy, Suspense, useMemo } from "react" +import { toEpochMs } from "~/lib/utils" import { Node } from "slate" import type { MessageWithPinned } from "~/atoms/chat-query-atoms" import { GifEmbed } from "~/components/gif-embed" @@ -266,7 +267,7 @@ function Embeds() { ) : null @@ -284,7 +285,7 @@ function Embeds() { key={url} url={url} author={message.author ?? undefined} - createdAt={message.createdAt.getTime()} + createdAt={toEpochMs(message.createdAt)} /> ))} diff --git a/apps/web/src/components/chat/message-item.tsx b/apps/web/src/components/chat/message-item.tsx index 961850463..b8c67750f 100644 --- a/apps/web/src/components/chat/message-item.tsx +++ b/apps/web/src/components/chat/message-item.tsx @@ -69,6 +69,7 @@ export const MessageItem = memo(function MessageItem({ // It's now implemented inside Message.Header but we keep this export import { format } from "date-fns" import IconPin from "~/components/icons/icon-pin" +import { toDate, toEpochMs } from "~/lib/utils" import { StatusEmojiWithTooltip } from "~/components/status/user-status-badge" import { Badge } from "~/components/ui/badge" import { useUserStatus } from "~/hooks/use-presence" @@ -84,7 +85,7 @@ export const MessageAuthorHeader = ({ const user = message.author const { statusEmoji, customMessage, statusExpiresAt, quietHours } = useUserStatus(message.authorId) - const isEdited = message.updatedAt && message.updatedAt.getTime() > message.createdAt.getTime() + const isEdited = message.updatedAt && toEpochMs(message.updatedAt) > toEpochMs(message.createdAt) if (!user) return null @@ -101,7 +102,7 @@ export const MessageAuthorHeader = ({ /> {user.userType === "machine" && APP} - {format(message.createdAt, "HH:mm")} + {format(toDate(message.createdAt), "HH:mm")} {isEdited && " (edited)"} {isPinned && ( diff --git a/apps/web/src/components/chat/message-list.tsx b/apps/web/src/components/chat/message-list.tsx index 6c00d8671..cac03b325 100644 --- a/apps/web/src/components/chat/message-list.tsx +++ b/apps/web/src/components/chat/message-list.tsx @@ -8,6 +8,7 @@ import { editingMessageAtomFamily } from "~/atoms/chat-atoms" import type { MessageWithPinned, ProcessedMessage } from "~/atoms/chat-query-atoms" import { useVisibleMessageNotificationCleaner } from "~/hooks/use-visible-message-notification-cleaner" import { useMessageToolbarOverlay } from "~/hooks/use-message-toolbar-overlay" +import { toEpochMs, toDate } from "~/lib/utils" import { MessageHoverProvider, useMessageHover } from "~/providers/message-hover-provider" import { Route } from "~/routes/_app/$orgSlug/chat/$id" @@ -193,13 +194,13 @@ function MessageListContent({ const isGroupStart = !prevMessage || message.authorId !== prevMessage.authorId || - message.createdAt.getTime() - prevMessage.createdAt.getTime() > timeThreshold || + toEpochMs(message.createdAt) - toEpochMs(prevMessage.createdAt) > timeThreshold || !!prevMessage.replyToMessageId const isGroupEnd = !nextMessage || message.authorId !== nextMessage.authorId || - nextMessage.createdAt.getTime() - message.createdAt.getTime() > timeThreshold + toEpochMs(nextMessage.createdAt) - toEpochMs(message.createdAt) > timeThreshold const isFirstNewMessage = false const isPinned = !!message.pinnedMessage?.id @@ -222,7 +223,7 @@ function MessageListContent({ let lastDate = "" for (const processedMessage of processedMessages) { - const date = new Date(processedMessage.message.createdAt).toDateString() + const date = toDate(processedMessage.message.createdAt).toDateString() if (date !== lastDate) { rows.push({ id: `header-${date}`, type: "header", date }) sticky.push(idx) diff --git a/apps/web/src/components/chat/message.tsx b/apps/web/src/components/chat/message.tsx index 0e2b0c559..3516fa33f 100644 --- a/apps/web/src/components/chat/message.tsx +++ b/apps/web/src/components/chat/message.tsx @@ -15,7 +15,7 @@ import { Badge } from "~/components/ui/badge" import { useChatStable } from "~/hooks/use-chat" import { useUserStatus } from "~/hooks/use-presence" import { useAuth } from "~/lib/auth" -import { cn } from "~/lib/utils" +import { cn, toDate, toEpochMs } from "~/lib/utils" import { useChatAuthorIdentity, type ChatAuthorIdentity } from "./author-identity" import { InlineThreadPreview } from "./inline-thread-preview" import { MessageAttachments } from "./message-attachments" @@ -248,7 +248,7 @@ function MessageAvatar() { return (
- {format(message.createdAt, "HH:mm")} + {format(toDate(message.createdAt), "HH:mm")}
) } @@ -263,7 +263,7 @@ const MessageHeader = memo(function MessageHeader() { const isDiscordSyncedResult = useAtomValue(isDiscordSyncedMessageAtomFamily(message.id)) const isDiscordSynced = AsyncResult.getOrElse(isDiscordSyncedResult, () => []).length > 0 - const isEdited = message.updatedAt && message.updatedAt.getTime() > message.createdAt.getTime() + const isEdited = message.updatedAt && toEpochMs(message.updatedAt) > toEpochMs(message.createdAt) if (!showAvatar || !user) return null @@ -289,7 +289,7 @@ const MessageHeader = memo(function MessageHeader() { Synced from Discord ) : null} - {format(message.createdAt, "HH:mm")} + {format(toDate(message.createdAt), "HH:mm")} {isEdited && " (edited)"} {isPinned && ( diff --git a/apps/web/src/components/chat/pinned-messages-modal.tsx b/apps/web/src/components/chat/pinned-messages-modal.tsx index 220d1a037..c9ad6a2a9 100644 --- a/apps/web/src/components/chat/pinned-messages-modal.tsx +++ b/apps/web/src/components/chat/pinned-messages-modal.tsx @@ -3,6 +3,7 @@ import { useNavigate } from "@tanstack/react-router" import { eq, useLiveQuery } from "@tanstack/react-db" import { format } from "date-fns" import { useCallback, useMemo } from "react" +import { toDate, toEpochMs } from "~/lib/utils" import { messageCollection, pinnedMessageCollection, userCollection } from "~/db/collections" import { useChatStable } from "~/hooks/use-chat" import IconClose from "../icons/icon-close" @@ -44,7 +45,7 @@ export function PinnedMessagesModal() { const sortedPins = useMemo( () => [...(pinnedMessages || [])].sort( - (a, b) => a.pinned.pinnedAt.getTime() - b.pinned.pinnedAt.getTime(), + (a, b) => toEpochMs(a.pinned.pinnedAt) - toEpochMs(b.pinned.pinnedAt), ), [pinnedMessages], ) @@ -103,8 +104,8 @@ export function PinnedMessagesModal() { const user = pinnedMessage.message.author const isEdited = pinnedMessage.message.updatedAt && - pinnedMessage.message.updatedAt.getTime() > - pinnedMessage.message.createdAt.getTime() + toEpochMs(pinnedMessage.message.updatedAt) > + toEpochMs(pinnedMessage.message.createdAt) return (
@@ -160,7 +161,7 @@ export function PinnedMessagesModal() { {/* Pinned Date */}
Pinned{" "} - {format(pinnedMessage.pinned.pinnedAt, "MMM d 'at' h:mm a")} + {format(toDate(pinnedMessage.pinned.pinnedAt), "MMM d 'at' h:mm a")}
) diff --git a/apps/web/src/components/chat/thread-message-list.tsx b/apps/web/src/components/chat/thread-message-list.tsx index b9ab62bb8..5859fd8f6 100644 --- a/apps/web/src/components/chat/thread-message-list.tsx +++ b/apps/web/src/components/chat/thread-message-list.tsx @@ -5,6 +5,7 @@ import { useCallback, useMemo } from "react" import { createPortal } from "react-dom" import type { MessageWithPinned } from "~/atoms/chat-query-atoms" import { useMessageToolbarOverlay } from "~/hooks/use-message-toolbar-overlay" +import { toEpochMs } from "~/lib/utils" import { threadMessagesWithAuthorAtomFamily } from "~/atoms/message-atoms" import { MessageHoverProvider, useMessageHover } from "~/providers/message-hover-provider" import type { MessageGroupPosition } from "./message" @@ -67,14 +68,14 @@ function ThreadMessageListContent({ threadChannelId }: ThreadMessageListProps) { const isGroupStart = !prevMessage || message.authorId !== prevMessage.authorId || - message.createdAt.getTime() - prevMessage.createdAt.getTime() > timeThreshold || + toEpochMs(message.createdAt) - toEpochMs(prevMessage.createdAt) > timeThreshold || !!prevMessage.replyToMessageId const nextMessage = index < messages.length - 1 ? messages[index + 1] : null const isGroupEnd = !nextMessage || message.authorId !== nextMessage.authorId || - nextMessage.createdAt.getTime() - message.createdAt.getTime() > timeThreshold + toEpochMs(nextMessage.createdAt) - toEpochMs(message.createdAt) > timeThreshold // Determine group position let groupPosition: MessageGroupPosition diff --git a/apps/web/src/components/status/user-status-badge.tsx b/apps/web/src/components/status/user-status-badge.tsx index 0e99905ba..10f118894 100644 --- a/apps/web/src/components/status/user-status-badge.tsx +++ b/apps/web/src/components/status/user-status-badge.tsx @@ -1,3 +1,4 @@ +import type { DateTime } from "effect" import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip" import { cn } from "~/lib/utils" import { formatStatusExpiration } from "~/utils/status" @@ -53,7 +54,7 @@ interface QuietHoursInfo { interface StatusEmojiWithTooltipProps { emoji: string | null | undefined message?: string | null - expiresAt?: Date | null + expiresAt?: Date | DateTime.Utc | null className?: string quietHours?: QuietHoursInfo /** Set to false when inside another interactive element (e.g., MenuTrigger) to avoid nested buttons */ diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts index 5e1abdf42..f11bd9f0f 100644 --- a/apps/web/src/lib/utils.ts +++ b/apps/web/src/lib/utils.ts @@ -1,6 +1,23 @@ import { type ClassValue, clsx } from "clsx" +import { DateTime } from "effect" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +/** + * Convert a `Date | DateTime.Utc` to epoch milliseconds. + * Handles the Effect v4 change where Schema models may return `DateTime.Utc` instead of `Date`. + */ +export function toEpochMs(d: Date | DateTime.Utc): number { + return d instanceof Date ? d.getTime() : d.epochMilliseconds +} + +/** + * Convert a `Date | DateTime.Utc` to a plain `Date`. + * Useful when passing to `date-fns` or `new Date()`. + */ +export function toDate(d: Date | DateTime.Utc): Date { + return d instanceof Date ? d : new Date(d.epochMilliseconds) +} diff --git a/apps/web/src/utils/status.ts b/apps/web/src/utils/status.ts index 1db348e29..8d9b2c41e 100644 --- a/apps/web/src/utils/status.ts +++ b/apps/web/src/utils/status.ts @@ -66,11 +66,11 @@ export function getStatusLabel(status?: PresenceStatus | string): string { /** * Formats the status expiration time in a human-readable way */ -export function formatStatusExpiration(expiresAt: Date | null | undefined): string | null { +export function formatStatusExpiration(expiresAt: Date | { epochMilliseconds: number } | null | undefined): string | null { if (!expiresAt) return null const now = new Date() - const expiry = new Date(expiresAt) + const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt.epochMilliseconds) // If already expired, return null if (expiry <= now) return null From ab00c6aed86cc821a200ebe10453d0d1e5f63202 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 14:44:27 +0100 Subject: [PATCH 23/34] fix --- .../src/policies/attachment-policy.test.ts | 111 ++++++++++++++---- apps/backend/src/policies/bot-policy.test.ts | 70 ++++++++--- .../policies/channel-member-policy.test.ts | 79 +++++++++---- .../src/policies/channel-policy.test.ts | 34 ++++-- .../integration-connection-policy.test.ts | 23 +++- .../src/policies/invitation-policy.test.ts | 88 +++++++++++--- .../src/policies/message-policy.test.ts | 73 +++++++++--- .../policies/message-reaction-policy.test.ts | 87 ++++++++++---- .../src/policies/policy-test-helpers.ts | 5 +- .../chat-sync-attribution-reconciler.test.ts | 52 ++++---- .../chat-sync/chat-sync-core-worker.ts | 9 +- .../services/chat-sync/discord-sync-worker.ts | 8 +- .../message-outbox-dispatcher.test.ts | 2 +- .../src/services/message-outbox-dispatcher.ts | 2 +- .../backend/src/services/org-resolver.test.ts | 33 +++--- apps/backend/src/services/session-manager.ts | 9 +- apps/backend/src/test/effect-helpers.ts | 14 +-- .../src/test/message-outbox-repo.test.ts | 6 +- .../channel-files-documents-list.tsx | 3 +- .../channel-files-media-gallery-view.tsx | 3 +- .../channel-files-media-grid.tsx | 3 +- .../components/chat/inline-thread-preview.tsx | 3 +- .../components/chat/message-attachments.tsx | 3 +- apps/web/src/components/chat/thread-panel.tsx | 3 +- .../command-palette/search-result-item.tsx | 4 +- .../src/components/sidebar/channel-item.tsx | 14 ++- apps/web/src/hooks/use-presence.ts | 6 +- .../web/src/lib/notifications/orchestrator.ts | 5 +- .../_app/$orgSlug/settings/invitations.tsx | 3 +- apps/web/src/utils/presence.ts | 13 +- packages/auth/src/consumers/backend-auth.ts | 4 +- 31 files changed, 551 insertions(+), 221 deletions(-) diff --git a/apps/backend/src/policies/attachment-policy.test.ts b/apps/backend/src/policies/attachment-policy.test.ts index 8f96aec2b..670e23d34 100644 --- a/apps/backend/src/policies/attachment-policy.test.ts +++ b/apps/backend/src/policies/attachment-policy.test.ts @@ -4,11 +4,14 @@ import type { AttachmentId, ChannelId, MessageId, OrganizationId, UserId } from import { Effect, Result, Layer, Option } from "effect" import { AttachmentPolicy } from "./attachment-policy.ts" import { + buildServiceLayer, makeActor, makeEntityNotFound, makeOrganizationMemberRepoLayer, makeOrgResolverLayer, runWithActorEither, + serviceEffect, + serviceShape, TEST_ORG_ID, TEST_USER_ID, } from "./policy-test-helpers.ts" @@ -25,7 +28,7 @@ const MESSAGE_AUTHOR_ID = "00000000-0000-0000-0000-000000000836" as UserId const makeAttachmentRepoLayer = ( attachments: Record, ) => - Layer.succeed(AttachmentRepo, { + Layer.succeed(AttachmentRepo, serviceShape({ with: ( id: AttachmentId, f: (attachment: { uploadedBy: UserId; messageId: MessageId | null }) => Effect.Effect, @@ -36,10 +39,10 @@ const makeAttachmentRepoLayer = ( } return f(attachment) }, - } as unknown as AttachmentRepo) + })) const makeMessageRepoLayer = (messages: Record) => - Layer.succeed(MessageRepo, { + Layer.succeed(MessageRepo, serviceShape({ with: ( id: MessageId, f: (message: { authorId: UserId; channelId: ChannelId }) => Effect.Effect, @@ -50,12 +53,12 @@ const makeMessageRepoLayer = (messages: Record, ) => - Layer.succeed(ChannelRepo, { + Layer.succeed(ChannelRepo, serviceShape({ with: ( id: ChannelId, f: (channel: { @@ -70,15 +73,15 @@ const makeChannelRepoLayer = ( } return f(channel) }, - } as unknown as ChannelRepo) + })) const makeChannelMemberRepoLayer = (memberships: Record) => - Layer.succeed(ChannelMemberRepo, { + Layer.succeed(ChannelMemberRepo, serviceShape({ findByChannelAndUser: (channelId: ChannelId, userId: UserId) => { const key = `${channelId}:${userId}` return Effect.succeed(memberships[key] ? Option.some({ channelId, userId }) : Option.none()) }, - } as unknown as ChannelMemberRepo) + })) const makePolicyLayer = (opts: { members?: Record @@ -87,7 +90,7 @@ const makePolicyLayer = (opts: { channels?: Record channelMemberships?: Record }) => - AttachmentPolicy.DefaultWithoutDependencies.pipe( + buildServiceLayer(AttachmentPolicy).pipe( Layer.provide(makeAttachmentRepoLayer(opts.attachments ?? {})), Layer.provide(makeMessageRepoLayer(opts.messages ?? {})), Layer.provide(makeChannelRepoLayer(opts.channels ?? {})), @@ -101,7 +104,11 @@ describe("AttachmentPolicy", () => { const actor = makeActor() const layer = makePolicyLayer({}) - const result = await runWithActorEither(AttachmentPolicy.canCreate(), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canCreate()), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -113,7 +120,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canUpdate(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canUpdate(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -125,7 +136,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canUpdate(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canUpdate(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -137,7 +152,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canDelete(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canDelete(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -149,7 +168,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canDelete(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canDelete(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -167,7 +190,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canDelete(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canDelete(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -185,7 +212,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canDelete(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canDelete(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -206,7 +237,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canDelete(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canDelete(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -227,7 +262,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canDelete(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canDelete(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -239,7 +278,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canView(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canView(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -251,7 +294,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canView(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canView(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -272,7 +319,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canView(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canView(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -293,7 +344,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canView(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canView(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -314,7 +369,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canView(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canView(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -335,7 +394,11 @@ describe("AttachmentPolicy", () => { }, }) - const result = await runWithActorEither(AttachmentPolicy.canView(ATTACHMENT_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(AttachmentPolicy, (policy) => policy.canView(ATTACHMENT_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) }) diff --git a/apps/backend/src/policies/bot-policy.test.ts b/apps/backend/src/policies/bot-policy.test.ts index d6d7f8343..6f0731d61 100644 --- a/apps/backend/src/policies/bot-policy.test.ts +++ b/apps/backend/src/policies/bot-policy.test.ts @@ -2,13 +2,15 @@ import { describe, expect, it } from "@effect/vitest" import { BotRepo } from "@hazel/backend-core" import { UnauthorizedError } from "@hazel/domain" import type { BotId, UserId } from "@hazel/schema" -import { Effect, Result, Layer } from "effect" +import { Effect, Result, Layer, ServiceMap } from "effect" import { BotPolicy } from "./bot-policy.ts" import { + buildServiceLayer, makeActor, makeEntityNotFound, makeOrgResolverLayer, runWithActorEither, + serviceEffect, TEST_ALT_ORG_ID, TEST_ORG_ID, } from "./policy-test-helpers.ts" @@ -27,10 +29,10 @@ const makeBotRepoLayer = (bots: Record) => } return f(bot) }, - } as unknown as BotRepo) + } as ServiceMap.Service.Shape) const makePolicyLayer = (members: Record, bots: Record) => - BotPolicy.DefaultWithoutDependencies.pipe( + buildServiceLayer(BotPolicy).pipe( Layer.provide(makeOrgResolverLayer(members)), Layer.provide(makeBotRepoLayer(bots)), ) @@ -45,8 +47,16 @@ describe("BotPolicy", () => { {}, ) - const allowed = await runWithActorEither(BotPolicy.canCreate(TEST_ORG_ID), layer, actor) - const denied = await runWithActorEither(BotPolicy.canCreate(TEST_ALT_ORG_ID), layer, actor) + const allowed = await runWithActorEither( + serviceEffect(BotPolicy, (policy) => policy.canCreate(TEST_ORG_ID)), + layer, + actor, + ) + const denied = await runWithActorEither( + serviceEffect(BotPolicy, (policy) => policy.canCreate(TEST_ALT_ORG_ID)), + layer, + actor, + ) expect(Result.isSuccess(allowed)).toBe(true) expect(Result.isFailure(denied)).toBe(true) @@ -72,13 +82,21 @@ describe("BotPolicy", () => { }, ) - const creatorAllowed = await runWithActorEither(BotPolicy.canRead(BOT_ID), layer, creator) + const creatorAllowed = await runWithActorEither( + serviceEffect(BotPolicy, (policy) => policy.canRead(BOT_ID)), + layer, + creator, + ) const adminAllowed = await runWithActorEither( - BotPolicy.canRead(BOT_ID), + serviceEffect(BotPolicy, (policy) => policy.canRead(BOT_ID)), layer, makeActor({ ...admin, organizationId: TEST_ORG_ID }), ) - const outsiderDenied = await runWithActorEither(BotPolicy.canRead(BOT_ID), layer, outsider) + const outsiderDenied = await runWithActorEither( + serviceEffect(BotPolicy, (policy) => policy.canRead(BOT_ID)), + layer, + outsider, + ) expect(Result.isSuccess(creatorAllowed)).toBe(true) expect(Result.isSuccess(adminAllowed)).toBe(true) @@ -92,9 +110,21 @@ describe("BotPolicy", () => { }) const layer = makePolicyLayer({}, { [BOT_ID]: { createdBy: creator.id } }) - const updateCreator = await runWithActorEither(BotPolicy.canUpdate(BOT_ID), layer, creator) - const updateOther = await runWithActorEither(BotPolicy.canUpdate(BOT_ID), layer, otherUser) - const deleteMissing = await runWithActorEither(BotPolicy.canDelete(MISSING_BOT_ID), layer, creator) + const updateCreator = await runWithActorEither( + serviceEffect(BotPolicy, (policy) => policy.canUpdate(BOT_ID)), + layer, + creator, + ) + const updateOther = await runWithActorEither( + serviceEffect(BotPolicy, (policy) => policy.canUpdate(BOT_ID)), + layer, + otherUser, + ) + const deleteMissing = await runWithActorEither( + serviceEffect(BotPolicy, (policy) => policy.canDelete(MISSING_BOT_ID)), + layer, + creator, + ) expect(Result.isSuccess(updateCreator)).toBe(true) expect(Result.isFailure(updateOther)).toBe(true) @@ -117,9 +147,21 @@ describe("BotPolicy", () => { {}, ) - const installAdmin = await runWithActorEither(BotPolicy.canInstall(TEST_ORG_ID), layer, admin) - const uninstallAdmin = await runWithActorEither(BotPolicy.canUninstall(TEST_ORG_ID), layer, admin) - const installMember = await runWithActorEither(BotPolicy.canInstall(TEST_ORG_ID), layer, member) + const installAdmin = await runWithActorEither( + serviceEffect(BotPolicy, (policy) => policy.canInstall(TEST_ORG_ID)), + layer, + admin, + ) + const uninstallAdmin = await runWithActorEither( + serviceEffect(BotPolicy, (policy) => policy.canUninstall(TEST_ORG_ID)), + layer, + admin, + ) + const installMember = await runWithActorEither( + serviceEffect(BotPolicy, (policy) => policy.canInstall(TEST_ORG_ID)), + layer, + member, + ) expect(Result.isSuccess(installAdmin)).toBe(true) expect(Result.isSuccess(uninstallAdmin)).toBe(true) diff --git a/apps/backend/src/policies/channel-member-policy.test.ts b/apps/backend/src/policies/channel-member-policy.test.ts index b11becd48..587662106 100644 --- a/apps/backend/src/policies/channel-member-policy.test.ts +++ b/apps/backend/src/policies/channel-member-policy.test.ts @@ -5,11 +5,14 @@ import type { ChannelId, ChannelMemberId, OrganizationId, UserId } from "@hazel/ import { Effect, Result, Layer, Option } from "effect" import { ChannelMemberPolicy } from "./channel-member-policy.ts" import { + buildServiceLayer, makeActor, makeEntityNotFound, makeOrganizationMemberRepoLayer, makeOrgResolverLayer, runWithActorEither, + serviceEffect, + serviceShape, TEST_ORG_ID, TEST_USER_ID, } from "./policy-test-helpers.ts" @@ -36,7 +39,7 @@ const makeChannelMemberRepoLayer = ( channelMembers: Record, membershipsByChannelAndUser: Record = {}, ) => - Layer.succeed(ChannelMemberRepo, { + Layer.succeed(ChannelMemberRepo, serviceShape({ with: (id: ChannelMemberId, f: (member: ChannelMemberEntry) => Effect.Effect) => { const member = channelMembers[id] if (!member) { @@ -49,10 +52,10 @@ const makeChannelMemberRepoLayer = ( const entry = membershipsByChannelAndUser[key] return Effect.succeed(entry ? Option.some(entry) : Option.none()) }, - } as unknown as ChannelMemberRepo) + })) const makeChannelRepoLayer = (channels: Record) => - Layer.succeed(ChannelRepo, { + Layer.succeed(ChannelRepo, serviceShape({ with: (id: ChannelId, f: (channel: ChannelEntry) => Effect.Effect) => { const channel = channels[id] if (!channel) { @@ -60,7 +63,7 @@ const makeChannelRepoLayer = (channels: Record) => } return f(channel) }, - } as unknown as ChannelRepo) + })) const makePolicyLayer = (opts: { members: Record @@ -68,7 +71,7 @@ const makePolicyLayer = (opts: { channelMembers?: Record membershipsByChannelAndUser?: Record }) => - ChannelMemberPolicy.DefaultWithoutDependencies.pipe( + buildServiceLayer(ChannelMemberPolicy).pipe( Layer.provide( makeChannelMemberRepoLayer(opts.channelMembers ?? {}, opts.membershipsByChannelAndUser ?? {}), ), @@ -88,7 +91,11 @@ describe("ChannelMemberPolicy", () => { }, }) - const result = await runWithActorEither(ChannelMemberPolicy.isOwner(CHANNEL_MEMBER_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(ChannelMemberPolicy, (policy) => policy.isOwner(CHANNEL_MEMBER_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -102,7 +109,11 @@ describe("ChannelMemberPolicy", () => { }, }) - const result = await runWithActorEither(ChannelMemberPolicy.isOwner(CHANNEL_MEMBER_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(ChannelMemberPolicy, (policy) => policy.isOwner(CHANNEL_MEMBER_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.failure)).toBe(true) @@ -123,8 +134,16 @@ describe("ChannelMemberPolicy", () => { }, }) - const adminResult = await runWithActorEither(ChannelMemberPolicy.canCreate(CHANNEL_ID), layer, admin) - const ownerResult = await runWithActorEither(ChannelMemberPolicy.canCreate(CHANNEL_ID), layer, owner) + const adminResult = await runWithActorEither( + serviceEffect(ChannelMemberPolicy, (policy) => policy.canCreate(CHANNEL_ID)), + layer, + admin, + ) + const ownerResult = await runWithActorEither( + serviceEffect(ChannelMemberPolicy, (policy) => policy.canCreate(CHANNEL_ID)), + layer, + owner, + ) expect(Result.isSuccess(adminResult)).toBe(true) expect(Result.isSuccess(ownerResult)).toBe(true) @@ -141,7 +160,11 @@ describe("ChannelMemberPolicy", () => { }, }) - const result = await runWithActorEither(ChannelMemberPolicy.canCreate(CHANNEL_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(ChannelMemberPolicy, (policy) => policy.canCreate(CHANNEL_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -156,7 +179,11 @@ describe("ChannelMemberPolicy", () => { }, }) - const result = await runWithActorEither(ChannelMemberPolicy.canCreate(CHANNEL_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(ChannelMemberPolicy, (policy) => policy.canCreate(CHANNEL_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.failure)).toBe(true) @@ -177,7 +204,11 @@ describe("ChannelMemberPolicy", () => { }, }) - const result = await runWithActorEither(ChannelMemberPolicy.canRead(CHANNEL_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(ChannelMemberPolicy, (policy) => policy.canRead(CHANNEL_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -193,7 +224,11 @@ describe("ChannelMemberPolicy", () => { // No channel membership for admin }) - const result = await runWithActorEither(ChannelMemberPolicy.canRead(CHANNEL_ID), layer, admin) + const result = await runWithActorEither( + serviceEffect(ChannelMemberPolicy, (policy) => policy.canRead(CHANNEL_ID)), + layer, + admin, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -209,7 +244,11 @@ describe("ChannelMemberPolicy", () => { // No channel membership }) - const result = await runWithActorEither(ChannelMemberPolicy.canRead(CHANNEL_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(ChannelMemberPolicy, (policy) => policy.canRead(CHANNEL_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.failure)).toBe(true) @@ -231,7 +270,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither( - ChannelMemberPolicy.canUpdate(CHANNEL_MEMBER_ID), + serviceEffect(ChannelMemberPolicy, (policy) => policy.canUpdate(CHANNEL_MEMBER_ID)), layer, actor, ) @@ -257,12 +296,12 @@ describe("ChannelMemberPolicy", () => { }) const adminResult = await runWithActorEither( - ChannelMemberPolicy.canUpdate(CHANNEL_MEMBER_ID), + serviceEffect(ChannelMemberPolicy, (policy) => policy.canUpdate(CHANNEL_MEMBER_ID)), layer, admin, ) const ownerResult = await runWithActorEither( - ChannelMemberPolicy.canUpdate(CHANNEL_MEMBER_ID), + serviceEffect(ChannelMemberPolicy, (policy) => policy.canUpdate(CHANNEL_MEMBER_ID)), layer, owner, ) @@ -291,7 +330,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither( - ChannelMemberPolicy.canDelete(CHANNEL_MEMBER_ID), + serviceEffect(ChannelMemberPolicy, (policy) => policy.canDelete(CHANNEL_MEMBER_ID)), layer, actor, ) @@ -317,12 +356,12 @@ describe("ChannelMemberPolicy", () => { }) const adminResult = await runWithActorEither( - ChannelMemberPolicy.canDelete(CHANNEL_MEMBER_ID), + serviceEffect(ChannelMemberPolicy, (policy) => policy.canDelete(CHANNEL_MEMBER_ID)), layer, admin, ) const ownerResult = await runWithActorEither( - ChannelMemberPolicy.canDelete(CHANNEL_MEMBER_ID), + serviceEffect(ChannelMemberPolicy, (policy) => policy.canDelete(CHANNEL_MEMBER_ID)), layer, owner, ) diff --git a/apps/backend/src/policies/channel-policy.test.ts b/apps/backend/src/policies/channel-policy.test.ts index caa2c67f1..91f8efdf3 100644 --- a/apps/backend/src/policies/channel-policy.test.ts +++ b/apps/backend/src/policies/channel-policy.test.ts @@ -2,13 +2,15 @@ import { describe, expect, it } from "@effect/vitest" import { ChannelRepo } from "@hazel/backend-core" import { UnauthorizedError } from "@hazel/domain" import type { ChannelId, OrganizationId } from "@hazel/schema" -import { Effect, Result, Layer } from "effect" +import { Effect, Result, Layer, ServiceMap } from "effect" import { ChannelPolicy } from "./channel-policy.ts" import { + buildServiceLayer, makeActor, makeEntityNotFound, makeOrgResolverLayer, runWithActorEither, + serviceEffect, TEST_ALT_ORG_ID, TEST_ORG_ID, } from "./policy-test-helpers.ts" @@ -30,13 +32,13 @@ const makeChannelRepoLayer = (channels: Record) const makePolicyLayer = ( members: Record, channels: Record, ) => - ChannelPolicy.DefaultWithoutDependencies.pipe( + buildServiceLayer(ChannelPolicy).pipe( Layer.provide(makeChannelRepoLayer(channels)), Layer.provide(makeOrgResolverLayer(members)), ) @@ -50,25 +52,25 @@ describe("ChannelPolicy", () => { const ownerLayer = makePolicyLayer({ [`${TEST_ORG_ID}:${actor.id}`]: "owner" }, {}) const memberResult = await runWithActorEither( - ChannelPolicy.canCreate(TEST_ORG_ID), + serviceEffect(ChannelPolicy, (policy) => policy.canCreate(TEST_ORG_ID)), memberLayer, actor, ["channels:write"], ) const adminResult = await runWithActorEither( - ChannelPolicy.canCreate(TEST_ORG_ID), + serviceEffect(ChannelPolicy, (policy) => policy.canCreate(TEST_ORG_ID)), adminLayer, actor, ["channels:write"], ) const ownerResult = await runWithActorEither( - ChannelPolicy.canCreate(TEST_ORG_ID), + serviceEffect(ChannelPolicy, (policy) => policy.canCreate(TEST_ORG_ID)), ownerLayer, actor, ["channels:write"], ) const noMembership = await runWithActorEither( - ChannelPolicy.canCreate(TEST_ALT_ORG_ID), + serviceEffect(ChannelPolicy, (policy) => policy.canCreate(TEST_ALT_ORG_ID)), memberLayer, actor, ["channels:write"], @@ -91,8 +93,16 @@ describe("ChannelPolicy", () => { }, ) - const allowed = await runWithActorEither(ChannelPolicy.canUpdate(CHANNEL_ID), layer, actor) - const missing = await runWithActorEither(ChannelPolicy.canUpdate(MISSING_CHANNEL_ID), layer, actor) + const allowed = await runWithActorEither( + serviceEffect(ChannelPolicy, (policy) => policy.canUpdate(CHANNEL_ID)), + layer, + actor, + ) + const missing = await runWithActorEither( + serviceEffect(ChannelPolicy, (policy) => policy.canUpdate(MISSING_CHANNEL_ID)), + layer, + actor, + ) expect(Result.isSuccess(allowed)).toBe(true) expect(Result.isFailure(missing)).toBe(true) @@ -112,7 +122,11 @@ describe("ChannelPolicy", () => { }, ) - const result = await runWithActorEither(ChannelPolicy.canDelete(CHANNEL_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(ChannelPolicy, (policy) => policy.canDelete(CHANNEL_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) }) diff --git a/apps/backend/src/policies/integration-connection-policy.test.ts b/apps/backend/src/policies/integration-connection-policy.test.ts index 6b8162d20..aea92527d 100644 --- a/apps/backend/src/policies/integration-connection-policy.test.ts +++ b/apps/backend/src/policies/integration-connection-policy.test.ts @@ -2,12 +2,19 @@ import { describe, expect, it } from "@effect/vitest" import { UnauthorizedError } from "@hazel/domain" import { Result, Layer } from "effect" import { IntegrationConnectionPolicy } from "./integration-connection-policy.ts" -import { makeActor, makeOrgResolverLayer, runWithActorEither, TEST_ORG_ID } from "./policy-test-helpers.ts" +import { + buildServiceLayer, + makeActor, + makeOrgResolverLayer, + runWithActorEither, + serviceEffect, + TEST_ORG_ID, +} from "./policy-test-helpers.ts" type Role = "admin" | "member" | "owner" const makePolicyLayer = (members: Record) => - IntegrationConnectionPolicy.DefaultWithoutDependencies.pipe(Layer.provide(makeOrgResolverLayer(members))) + buildServiceLayer(IntegrationConnectionPolicy).pipe(Layer.provide(makeOrgResolverLayer(members))) describe("IntegrationConnectionPolicy", () => { it("allows select for any org member", async () => { @@ -17,7 +24,7 @@ describe("IntegrationConnectionPolicy", () => { }) const result = await runWithActorEither( - IntegrationConnectionPolicy.canSelect(TEST_ORG_ID), + serviceEffect(IntegrationConnectionPolicy, (policy) => policy.canSelect(TEST_ORG_ID)), layer, actor, ) @@ -31,16 +38,20 @@ describe("IntegrationConnectionPolicy", () => { }) const insert = await runWithActorEither( - IntegrationConnectionPolicy.canInsert(TEST_ORG_ID), + serviceEffect(IntegrationConnectionPolicy, (policy) => policy.canInsert(TEST_ORG_ID)), layer, actor, ) const update = await runWithActorEither( - IntegrationConnectionPolicy.canUpdate(TEST_ORG_ID), + serviceEffect(IntegrationConnectionPolicy, (policy) => policy.canUpdate(TEST_ORG_ID)), + layer, + actor, + ) + const del = await runWithActorEither( + serviceEffect(IntegrationConnectionPolicy, (policy) => policy.canDelete(TEST_ORG_ID)), layer, actor, ) - const del = await runWithActorEither(IntegrationConnectionPolicy.canDelete(TEST_ORG_ID), layer, actor) expect(Result.isFailure(insert)).toBe(true) expect(Result.isFailure(update)).toBe(true) diff --git a/apps/backend/src/policies/invitation-policy.test.ts b/apps/backend/src/policies/invitation-policy.test.ts index 07c8f7812..e28651f7d 100644 --- a/apps/backend/src/policies/invitation-policy.test.ts +++ b/apps/backend/src/policies/invitation-policy.test.ts @@ -2,14 +2,16 @@ import { describe, expect, it } from "@effect/vitest" import { InvitationRepo, UserRepo } from "@hazel/backend-core" import { UnauthorizedError } from "@hazel/domain" import type { InvitationId, OrganizationId, UserId } from "@hazel/schema" -import { Effect, Result, Layer, Option } from "effect" +import { Effect, Result, Layer, Option, ServiceMap } from "effect" import { InvitationPolicy } from "./invitation-policy.ts" import { + buildServiceLayer, makeActor, makeEntityNotFound, makeOrganizationMemberRepoLayer, makeOrgResolverLayer, runWithActorEither, + serviceEffect, TEST_ORG_ID, TEST_USER_ID, } from "./policy-test-helpers.ts" @@ -38,7 +40,7 @@ const makeInvitationRepoLayer = ( } return f(invitation) }, - } as unknown as InvitationRepo) + } as ServiceMap.Service.Shape) const makeUserRepoLayer = (users: Record) => Layer.succeed(UserRepo, { @@ -46,14 +48,14 @@ const makeUserRepoLayer = (users: Record) => const user = users[id] return Effect.succeed(user ? Option.some(user) : Option.none()) }, - } as unknown as UserRepo) + } as ServiceMap.Service.Shape) const makePolicyLayer = ( members: Record, invitations: Record, users: Record = {}, ) => - InvitationPolicy.DefaultWithoutDependencies.pipe( + buildServiceLayer(InvitationPolicy).pipe( Layer.provide(makeOrgResolverLayer(members)), Layer.provide(makeOrganizationMemberRepoLayer(members)), Layer.provide(makeInvitationRepoLayer(invitations)), @@ -65,7 +67,11 @@ describe("InvitationPolicy", () => { const actor = makeActor() const layer = makePolicyLayer({}, {}) - const result = await runWithActorEither(InvitationPolicy.canRead(INVITATION_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canRead(INVITATION_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -79,7 +85,11 @@ describe("InvitationPolicy", () => { {}, ) - const result = await runWithActorEither(InvitationPolicy.canCreate(TEST_ORG_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canCreate(TEST_ORG_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -93,7 +103,11 @@ describe("InvitationPolicy", () => { {}, ) - const result = await runWithActorEither(InvitationPolicy.canCreate(TEST_ORG_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canCreate(TEST_ORG_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -111,7 +125,11 @@ describe("InvitationPolicy", () => { }, ) - const result = await runWithActorEither(InvitationPolicy.canUpdate(INVITATION_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canUpdate(INVITATION_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -131,7 +149,11 @@ describe("InvitationPolicy", () => { }, ) - const result = await runWithActorEither(InvitationPolicy.canUpdate(INVITATION_ID), layer, admin) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canUpdate(INVITATION_ID)), + layer, + admin, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -151,7 +173,11 @@ describe("InvitationPolicy", () => { }, ) - const result = await runWithActorEither(InvitationPolicy.canUpdate(INVITATION_ID), layer, outsider) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canUpdate(INVITATION_ID)), + layer, + outsider, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -169,7 +195,11 @@ describe("InvitationPolicy", () => { }, ) - const result = await runWithActorEither(InvitationPolicy.canDelete(INVITATION_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canDelete(INVITATION_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -189,7 +219,11 @@ describe("InvitationPolicy", () => { }, ) - const result = await runWithActorEither(InvitationPolicy.canDelete(INVITATION_ID), layer, admin) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canDelete(INVITATION_ID)), + layer, + admin, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -210,7 +244,11 @@ describe("InvitationPolicy", () => { }, ) - const result = await runWithActorEither(InvitationPolicy.canAccept(INVITATION_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canAccept(INVITATION_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -231,7 +269,11 @@ describe("InvitationPolicy", () => { }, ) - const result = await runWithActorEither(InvitationPolicy.canAccept(INVITATION_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canAccept(INVITATION_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -250,7 +292,11 @@ describe("InvitationPolicy", () => { {}, ) - const result = await runWithActorEither(InvitationPolicy.canAccept(INVITATION_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canAccept(INVITATION_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -264,7 +310,11 @@ describe("InvitationPolicy", () => { {}, ) - const result = await runWithActorEither(InvitationPolicy.canList(TEST_ORG_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canList(TEST_ORG_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -278,7 +328,11 @@ describe("InvitationPolicy", () => { {}, ) - const result = await runWithActorEither(InvitationPolicy.canList(TEST_ORG_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(InvitationPolicy, (policy) => policy.canList(TEST_ORG_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) diff --git a/apps/backend/src/policies/message-policy.test.ts b/apps/backend/src/policies/message-policy.test.ts index 89b16dd56..7bec2e29b 100644 --- a/apps/backend/src/policies/message-policy.test.ts +++ b/apps/backend/src/policies/message-policy.test.ts @@ -6,10 +6,13 @@ import { Effect, Result, Layer, Option } from "effect" import { OrgResolver } from "../services/org-resolver.ts" import { MessagePolicy } from "./message-policy.ts" import { + buildServiceLayer, makeActor, makeEntityNotFound, makeOrganizationMemberRepoLayer, runWithActorEither, + serviceEffect, + serviceShape, TEST_ORG_ID, TEST_USER_ID, } from "./policy-test-helpers.ts" @@ -26,7 +29,7 @@ const MISSING_MESSAGE_ID = "00000000-0000-0000-0000-000000000899" as MessageId const makeChannelRepoLayer = ( channels: Record, ) => - Layer.succeed(ChannelRepo, { + Layer.succeed(ChannelRepo, serviceShape({ findById: (id: ChannelId) => { const channel = channels[id] return Effect.succeed(channel ? Option.some(channel) : Option.none()) @@ -45,13 +48,13 @@ const makeChannelRepoLayer = ( } return f(channel) }, - } as unknown as ChannelRepo) + })) /** * Creates a MessageRepo mock with a `with` method. */ const makeMessageRepoLayer = (messages: Record) => - Layer.succeed(MessageRepo, { + Layer.succeed(MessageRepo, serviceShape({ findById: (id: MessageId) => { const message = messages[id] return Effect.succeed(message ? Option.some(message) : Option.none()) @@ -66,11 +69,11 @@ const makeMessageRepoLayer = (messages: Record({ findByChannelAndUser: (_channelId: ChannelId, _userId: UserId) => Effect.succeed(Option.none()), -} as unknown as ChannelMemberRepo) +})) /** * Builds the full layer stack for MessagePolicy tests. @@ -85,14 +88,14 @@ const makePolicyLayer = ( const messageRepoLayer = makeMessageRepoLayer(messages) const orgMemberRepoLayer = makeOrganizationMemberRepoLayer(members) - const orgResolverLayer = OrgResolver.DefaultWithoutDependencies.pipe( + const orgResolverLayer = buildServiceLayer(OrgResolver).pipe( Layer.provide(orgMemberRepoLayer), Layer.provide(channelRepoLayer), Layer.provide(emptyChannelMemberRepoLayer), Layer.provide(messageRepoLayer), ) - return MessagePolicy.DefaultWithoutDependencies.pipe( + return buildServiceLayer(MessagePolicy).pipe( Layer.provide(orgResolverLayer), Layer.provide(messageRepoLayer), Layer.provide(channelRepoLayer), @@ -113,7 +116,11 @@ describe("MessagePolicy", () => { }, ) - const result = await runWithActorEither(MessagePolicy.canCreate(CHANNEL_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(MessagePolicy, (policy) => policy.canCreate(CHANNEL_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -129,7 +136,11 @@ describe("MessagePolicy", () => { }, ) - const result = await runWithActorEither(MessagePolicy.canCreate(CHANNEL_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(MessagePolicy, (policy) => policy.canCreate(CHANNEL_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -145,7 +156,11 @@ describe("MessagePolicy", () => { }, ) - const result = await runWithActorEither(MessagePolicy.canRead(CHANNEL_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(MessagePolicy, (policy) => policy.canRead(CHANNEL_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -163,7 +178,11 @@ describe("MessagePolicy", () => { }, ) - const result = await runWithActorEither(MessagePolicy.canUpdate(MESSAGE_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(MessagePolicy, (policy) => policy.canUpdate(MESSAGE_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -183,7 +202,11 @@ describe("MessagePolicy", () => { }, ) - const result = await runWithActorEither(MessagePolicy.canUpdate(MESSAGE_ID), layer, otherUser) + const result = await runWithActorEither( + serviceEffect(MessagePolicy, (policy) => policy.canUpdate(MESSAGE_ID)), + layer, + otherUser, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -201,7 +224,11 @@ describe("MessagePolicy", () => { }, ) - const result = await runWithActorEither(MessagePolicy.canDelete(MESSAGE_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(MessagePolicy, (policy) => policy.canDelete(MESSAGE_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -221,7 +248,11 @@ describe("MessagePolicy", () => { }, ) - const result = await runWithActorEither(MessagePolicy.canDelete(MESSAGE_ID), layer, admin) + const result = await runWithActorEither( + serviceEffect(MessagePolicy, (policy) => policy.canDelete(MESSAGE_ID)), + layer, + admin, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -241,7 +272,11 @@ describe("MessagePolicy", () => { }, ) - const result = await runWithActorEither(MessagePolicy.canDelete(MESSAGE_ID), layer, member) + const result = await runWithActorEither( + serviceEffect(MessagePolicy, (policy) => policy.canDelete(MESSAGE_ID)), + layer, + member, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -257,7 +292,11 @@ describe("MessagePolicy", () => { }, ) - const result = await runWithActorEither(MessagePolicy.canDelete(MISSING_MESSAGE_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(MessagePolicy, (policy) => policy.canDelete(MISSING_MESSAGE_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.failure)).toBe(true) diff --git a/apps/backend/src/policies/message-reaction-policy.test.ts b/apps/backend/src/policies/message-reaction-policy.test.ts index 0d4793ee6..b8e150ca2 100644 --- a/apps/backend/src/policies/message-reaction-policy.test.ts +++ b/apps/backend/src/policies/message-reaction-policy.test.ts @@ -19,7 +19,15 @@ import { Effect, Result, Layer, Option } from "effect" import { MessageReactionPolicy } from "./message-reaction-policy.ts" import { ConnectConversationService } from "../services/connect-conversation-service.ts" import { OrgResolver } from "../services/org-resolver.ts" -import { makeActor, makeEntityNotFound, runWithActorEither, TEST_ORG_ID } from "./policy-test-helpers.ts" +import { + buildServiceLayer, + makeActor, + makeEntityNotFound, + runWithActorEither, + serviceEffect, + serviceShape, + TEST_ORG_ID, +} from "./policy-test-helpers.ts" type Role = "admin" | "member" | "owner" @@ -33,16 +41,16 @@ type MessageData = { channelId: ChannelId } type ChannelData = { organizationId: OrganizationId; type: string; id: string } const makeReactionRepoLayer = (reactions: Record) => - Layer.succeed(MessageReactionRepo, { + Layer.succeed(MessageReactionRepo, serviceShape({ with: (id: MessageReactionId, f: (r: ReactionData) => Effect.Effect) => { const reaction = reactions[id] if (!reaction) return Effect.fail(makeEntityNotFound("MessageReaction")) return f(reaction) }, - } as unknown as MessageReactionRepo) + })) const makeMessageRepoLayer = (messages: Record) => - Layer.succeed(MessageRepo, { + Layer.succeed(MessageRepo, serviceShape({ with: (id: MessageId, f: (m: MessageData) => Effect.Effect) => { const message = messages[id] if (!message) return Effect.fail(makeEntityNotFound("Message")) @@ -52,35 +60,38 @@ const makeMessageRepoLayer = (messages: Record) => const message = messages[id] return Effect.succeed(message ? Option.some(message) : Option.none()) }, - } as unknown as MessageRepo) + })) const makeChannelRepoLayer = (channels: Record) => - Layer.succeed(ChannelRepo, { + Layer.succeed(ChannelRepo, serviceShape({ findById: (id: ChannelId) => { const channel = channels[id] return Effect.succeed(channel ? Option.some(channel) : Option.none()) }, - } as unknown as ChannelRepo) + })) const makeOrgMemberRepoLayer = (orgMembers: Record) => - Layer.succeed(OrganizationMemberRepo, { + Layer.succeed(OrganizationMemberRepo, serviceShape({ findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { const role = orgMembers[`${organizationId}:${userId}`] return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) }, - } as unknown as OrganizationMemberRepo) + })) -const emptyChannelMemberRepoLayer = Layer.succeed(ChannelMemberRepo, { +const emptyChannelMemberRepoLayer = Layer.succeed(ChannelMemberRepo, serviceShape({ findByChannelAndUser: (_channelId: ChannelId, _userId: UserId) => Effect.succeed(Option.none()), -} as unknown as ChannelMemberRepo) +})) -const emptyMessageRepoLayer = Layer.succeed(MessageRepo, { +const emptyMessageRepoLayer = Layer.succeed(MessageRepo, serviceShape({ findById: (_id: MessageId) => Effect.succeed(Option.none()), -} as unknown as MessageRepo) +})) -const connectConversationServiceLayer = Layer.succeed(ConnectConversationService, { +const connectConversationServiceLayer = Layer.succeed( + ConnectConversationService, + serviceShape({ canAccessConversation: () => Effect.succeed(false), -} as unknown as ConnectConversationService) + }), +) const makePolicyLayer = ( orgMembers: Record, @@ -93,14 +104,14 @@ const makePolicyLayer = ( const orgMemberRepoLayer = makeOrgMemberRepoLayer(orgMembers) // Build OrgResolver with actual channel data (not empty stubs) - const orgResolverLayer = OrgResolver.DefaultWithoutDependencies.pipe( + const orgResolverLayer = buildServiceLayer(OrgResolver).pipe( Layer.provide(orgMemberRepoLayer), Layer.provide(channelRepoLayer), Layer.provide(emptyChannelMemberRepoLayer), Layer.provide(emptyMessageRepoLayer), ) - return MessageReactionPolicy.DefaultWithoutDependencies.pipe( + return buildServiceLayer(MessageReactionPolicy).pipe( Layer.provide(makeReactionRepoLayer(reactions)), Layer.provide(messageRepoLayer), Layer.provide(orgResolverLayer), @@ -113,7 +124,11 @@ describe("MessageReactionPolicy", () => { const actor = makeActor() const layer = makePolicyLayer({}, {}, {}, {}) - const result = await runWithActorEither(MessageReactionPolicy.canList(MESSAGE_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(MessageReactionPolicy, (policy) => policy.canList(MESSAGE_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -126,7 +141,11 @@ describe("MessageReactionPolicy", () => { { [CHANNEL_ID]: { organizationId: TEST_ORG_ID, type: "public", id: CHANNEL_ID } }, ) - const result = await runWithActorEither(MessageReactionPolicy.canCreate(MESSAGE_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(MessageReactionPolicy, (policy) => policy.canCreate(MESSAGE_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -139,7 +158,11 @@ describe("MessageReactionPolicy", () => { { [CHANNEL_ID]: { organizationId: TEST_ORG_ID, type: "public", id: CHANNEL_ID } }, ) - const result = await runWithActorEither(MessageReactionPolicy.canCreate(MESSAGE_ID), layer, outsider) + const result = await runWithActorEither( + serviceEffect(MessageReactionPolicy, (policy) => policy.canCreate(MESSAGE_ID)), + layer, + outsider, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -147,7 +170,11 @@ describe("MessageReactionPolicy", () => { const actor = makeActor() const layer = makePolicyLayer({}, { [REACTION_ID]: { userId: actor.id } }, {}, {}) - const result = await runWithActorEither(MessageReactionPolicy.canUpdate(REACTION_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(MessageReactionPolicy, (policy) => policy.canUpdate(REACTION_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -155,7 +182,11 @@ describe("MessageReactionPolicy", () => { const actor = makeActor() const layer = makePolicyLayer({}, { [REACTION_ID]: { userId: OTHER_USER_ID } }, {}, {}) - const result = await runWithActorEither(MessageReactionPolicy.canUpdate(REACTION_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(MessageReactionPolicy, (policy) => policy.canUpdate(REACTION_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -163,7 +194,11 @@ describe("MessageReactionPolicy", () => { const actor = makeActor() const layer = makePolicyLayer({}, { [REACTION_ID]: { userId: actor.id } }, {}, {}) - const result = await runWithActorEither(MessageReactionPolicy.canDelete(REACTION_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(MessageReactionPolicy, (policy) => policy.canDelete(REACTION_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -171,7 +206,11 @@ describe("MessageReactionPolicy", () => { const actor = makeActor() const layer = makePolicyLayer({}, { [REACTION_ID]: { userId: OTHER_USER_ID } }, {}, {}) - const result = await runWithActorEither(MessageReactionPolicy.canDelete(REACTION_ID), layer, actor) + const result = await runWithActorEither( + serviceEffect(MessageReactionPolicy, (policy) => policy.canDelete(REACTION_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.failure)).toBe(true) diff --git a/apps/backend/src/policies/policy-test-helpers.ts b/apps/backend/src/policies/policy-test-helpers.ts index b52865394..b0ce90f7a 100644 --- a/apps/backend/src/policies/policy-test-helpers.ts +++ b/apps/backend/src/policies/policy-test-helpers.ts @@ -5,6 +5,7 @@ import { CurrentRpcScopes } from "@hazel/domain/scopes" import type { ChannelId, ChannelMemberId, MessageId, OrganizationId, UserId } from "@hazel/schema" import { Effect, Layer, Option } from "effect" import { OrgResolver } from "../services/org-resolver" +export { buildServiceLayer, serviceEffect, serviceShape } from "../test/effect-helpers" export const TEST_ORG_ID = "00000000-0000-0000-0000-000000000001" as OrganizationId export const TEST_ALT_ORG_ID = "00000000-0000-0000-0000-000000000002" as OrganizationId @@ -26,7 +27,7 @@ export const makeActor = (overrides?: Partial): CurrentUser. export const runWithActorEither = ( make: Effect.Effect, - layer: Layer.Layer, + layer: Layer.Layer, actor: CurrentUser.Schema = makeActor(), scopes: ReadonlyArray = ["messages:read"], ) => @@ -85,7 +86,7 @@ const emptyMessageRepoLayer = Layer.succeed(MessageRepo, { * Provides stub repos for channels, channel members, and messages. */ export const makeOrgResolverLayer = (members: Record) => - OrgResolver.layer.pipe( + buildServiceLayer(OrgResolver).pipe( Layer.provide(makeOrganizationMemberRepoLayer(members)), Layer.provide(emptyChannelRepoLayer), Layer.provide(emptyChannelMemberRepoLayer), diff --git a/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.test.ts b/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.test.ts index 31060438b..7e4888ded 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.test.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.test.ts @@ -1,8 +1,9 @@ import { describe, expect, it } from "@effect/vitest" import { MessageRepo, OrganizationMemberRepo, UserRepo } from "@hazel/backend-core" import type { OrganizationId, UserId } from "@hazel/schema" -import { Effect, Layer } from "effect" +import { Effect, Layer, ServiceMap } from "effect" import { ChatSyncAttributionReconciler } from "./chat-sync-attribution-reconciler.ts" +import { buildServiceLayer, serviceEffect, serviceShape } from "../../test/effect-helpers" const ORGANIZATION_ID = "00000000-0000-0000-0000-000000000001" as OrganizationId const USER_ID = "00000000-0000-0000-0000-000000000002" as UserId @@ -13,10 +14,15 @@ const makeLayer = (deps: { userRepo: UserRepo organizationMemberRepo: OrganizationMemberRepo }) => - ChatSyncAttributionReconciler.DefaultWithoutDependencies.pipe( - Layer.provide(Layer.succeed(MessageRepo, deps.messageRepo)), - Layer.provide(Layer.succeed(UserRepo, deps.userRepo)), - Layer.provide(Layer.succeed(OrganizationMemberRepo, deps.organizationMemberRepo)), + buildServiceLayer(ChatSyncAttributionReconciler).pipe( + Layer.provide(Layer.succeed(MessageRepo, deps.messageRepo as ServiceMap.Service.Shape)), + Layer.provide(Layer.succeed(UserRepo, deps.userRepo as ServiceMap.Service.Shape)), + Layer.provide( + Layer.succeed( + OrganizationMemberRepo, + deps.organizationMemberRepo as ServiceMap.Service.Shape, + ), + ), ) describe("ChatSyncAttributionReconciler", () => { @@ -24,28 +30,30 @@ describe("ChatSyncAttributionReconciler", () => { let reassignParams: unknown = null const layer = makeLayer({ - messageRepo: { + messageRepo: serviceShape({ reassignExternalSyncedAuthors: (params: unknown) => { reassignParams = params return Effect.succeed(4) }, - } as unknown as MessageRepo, - userRepo: { + }), + userRepo: serviceShape({ upsertByExternalId: () => Effect.succeed({ id: SHADOW_USER_ID }), - } as unknown as UserRepo, - organizationMemberRepo: { + }), + organizationMemberRepo: serviceShape({ upsertByOrgAndUser: () => Effect.succeed({}), - } as unknown as OrganizationMemberRepo, + }), }) const result = await Effect.runPromise( - ChatSyncAttributionReconciler.relinkHistoricalProviderMessages({ + serviceEffect(ChatSyncAttributionReconciler, (service) => + service.relinkHistoricalProviderMessages({ organizationId: ORGANIZATION_ID, provider: "discord", userId: USER_ID, externalAccountId: "123", externalAccountName: "Maki", - }).pipe(Effect.provide(layer)), + }), + ).pipe(Effect.provide(layer)), ) expect(result.updatedCount).toBe(4) @@ -61,28 +69,30 @@ describe("ChatSyncAttributionReconciler", () => { let reassignParams: unknown = null const layer = makeLayer({ - messageRepo: { + messageRepo: serviceShape({ reassignExternalSyncedAuthors: (params: unknown) => { reassignParams = params return Effect.succeed(2) }, - } as unknown as MessageRepo, - userRepo: { + }), + userRepo: serviceShape({ upsertByExternalId: () => Effect.succeed({ id: SHADOW_USER_ID }), - } as unknown as UserRepo, - organizationMemberRepo: { + }), + organizationMemberRepo: serviceShape({ upsertByOrgAndUser: () => Effect.succeed({}), - } as unknown as OrganizationMemberRepo, + }), }) const result = await Effect.runPromise( - ChatSyncAttributionReconciler.unlinkHistoricalProviderMessages({ + serviceEffect(ChatSyncAttributionReconciler, (service) => + service.unlinkHistoricalProviderMessages({ organizationId: ORGANIZATION_ID, provider: "discord", userId: USER_ID, externalAccountId: "123", externalAccountName: "Maki", - }).pipe(Effect.provide(layer)), + }), + ).pipe(Effect.provide(layer)), ) expect(result.updatedCount).toBe(2) diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index ac14a612d..03f012469 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -151,6 +151,10 @@ export interface ChatSyncIngressThreadCreate { readonly dedupeKey?: string } +export class ChatSyncCoreWorker extends ServiceMap.Service()("ChatSyncCoreWorker", { + make: Effect.suspend((): Effect.Effect> => ChatSyncCoreWorkerMake), +}) {} + /** @internal — exported for integration tests that provide their own deps */ export const ChatSyncCoreWorkerMake = Effect.gen(function* () { const db = yield* Database.Database @@ -2735,11 +2739,6 @@ export const ChatSyncCoreWorkerMake = Effect.gen(function* () { } }) -export class ChatSyncCoreWorker extends ServiceMap.Service< - ChatSyncCoreWorker, - Effect.Effect.Success ->()("ChatSyncCoreWorker") {} - export const ChatSyncCoreWorkerLayer = Layer.effect(ChatSyncCoreWorker, ChatSyncCoreWorkerMake).pipe( Layer.provide(ChatSyncConnectionRepo.layer), Layer.provide(ChatSyncChannelLinkRepo.layer), diff --git a/apps/backend/src/services/chat-sync/discord-sync-worker.ts b/apps/backend/src/services/chat-sync/discord-sync-worker.ts index 47b5d1c91..32a44c761 100644 --- a/apps/backend/src/services/chat-sync/discord-sync-worker.ts +++ b/apps/backend/src/services/chat-sync/discord-sync-worker.ts @@ -32,6 +32,9 @@ export { DiscordSyncMessageNotFoundError, } +// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- tag-only service; real make provided via DiscordSyncWorkerMake / DiscordSyncWorkerLayer +export class DiscordSyncWorker extends ServiceMap.Service()("DiscordSyncWorker") {} + const DiscordSyncWorkerMake = Effect.gen(function* () { const coreWorker = yield* ChatSyncCoreWorker @@ -221,11 +224,6 @@ const DiscordSyncWorkerMake = Effect.gen(function* () { } }) -export class DiscordSyncWorker extends ServiceMap.Service< - DiscordSyncWorker, - Effect.Effect.Success ->()("DiscordSyncWorker") {} - export const DiscordSyncWorkerLayer = Layer.effect(DiscordSyncWorker, DiscordSyncWorkerMake).pipe( Layer.provide(ChatSyncCoreWorkerLayer), ) diff --git a/apps/backend/src/services/message-outbox-dispatcher.test.ts b/apps/backend/src/services/message-outbox-dispatcher.test.ts index 0841ed6a8..97ea047c6 100644 --- a/apps/backend/src/services/message-outbox-dispatcher.test.ts +++ b/apps/backend/src/services/message-outbox-dispatcher.test.ts @@ -38,7 +38,7 @@ const runRepoEffect = (harness: ChatSyncDbHarness, effect: Effect.Effec const runDispatcherEffect = ( harness: ChatSyncDbHarness, - sideEffects: MessageSideEffectService, + sideEffects: ServiceMap.Service.Shape, make: Effect.Effect, ) => Effect.runPromise( diff --git a/apps/backend/src/services/message-outbox-dispatcher.ts b/apps/backend/src/services/message-outbox-dispatcher.ts index 70b26aa6d..b409cf59c 100644 --- a/apps/backend/src/services/message-outbox-dispatcher.ts +++ b/apps/backend/src/services/message-outbox-dispatcher.ts @@ -169,7 +169,7 @@ export class MessageOutboxDispatcher extends ServiceMap.Service => + const campaignForLeadership = (): Effect.Effect => Effect.gen(function* () { const reservedResult = yield* Effect.tryPromise({ try: (): Promise => pool.connect(), diff --git a/apps/backend/src/services/org-resolver.test.ts b/apps/backend/src/services/org-resolver.test.ts index 2cadb1f1f..f6f8024ec 100644 --- a/apps/backend/src/services/org-resolver.test.ts +++ b/apps/backend/src/services/org-resolver.test.ts @@ -2,10 +2,11 @@ import { describe, expect, it } from "@effect/vitest" import { ChannelMemberRepo, ChannelRepo, MessageRepo, OrganizationMemberRepo } from "@hazel/backend-core" import { PermissionError } from "@hazel/domain" import type { ChannelId, ChannelMemberId, MessageId, OrganizationId, UserId } from "@hazel/schema" -import { Effect, Result, Layer, Option } from "effect" +import { Effect, Result, Layer, Option, ServiceMap } from "effect" import { OrgResolver } from "./org-resolver" import { makeActor, TEST_ORG_ID } from "../policies/policy-test-helpers" import { CurrentUser } from "@hazel/domain" +import { buildServiceLayer, serviceEffect, serviceShape } from "../test/effect-helpers" type Role = "admin" | "member" | "owner" @@ -14,12 +15,12 @@ const MESSAGE_ID = "00000000-0000-0000-0000-000000000601" as MessageId const CHANNEL_MEMBER_ID = "00000000-0000-0000-0000-000000000701" as ChannelMemberId const makeOrgMemberRepoLayer = (members: Record) => - Layer.succeed(OrganizationMemberRepo, { + Layer.succeed(OrganizationMemberRepo, serviceShape({ findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { const role = members[`${organizationId}:${userId}`] return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) }, - } as unknown as OrganizationMemberRepo) + })) const makeChannelRepoLayer = ( channels: Record< @@ -27,30 +28,30 @@ const makeChannelRepoLayer = ( { organizationId: OrganizationId; type: string; parentChannelId?: string | null; id: string } >, ) => - Layer.succeed(ChannelRepo, { + Layer.succeed(ChannelRepo, serviceShape({ findById: (id: ChannelId) => { const channel = channels[id] return Effect.succeed(channel ? Option.some(channel) : Option.none()) }, - } as unknown as ChannelRepo) + })) const makeChannelMemberRepoLayer = (memberships: Record) => - Layer.succeed(ChannelMemberRepo, { + Layer.succeed(ChannelMemberRepo, serviceShape({ findByChannelAndUser: (channelId: ChannelId, userId: UserId) => { const key = `${channelId}:${userId}` return Effect.succeed( memberships[key] ? Option.some({ id: CHANNEL_MEMBER_ID, channelId, userId }) : Option.none(), ) }, - } as unknown as ChannelMemberRepo) + })) const makeMessageRepoLayer = (messages: Record) => - Layer.succeed(MessageRepo, { + Layer.succeed(MessageRepo, serviceShape({ findById: (id: MessageId) => { const message = messages[id] return Effect.succeed(message ? Option.some(message) : Option.none()) }, - } as unknown as MessageRepo) + })) const makeResolverLayer = (opts: { members?: Record @@ -61,7 +62,7 @@ const makeResolverLayer = (opts: { channelMembers?: Record messages?: Record }) => - OrgResolver.DefaultWithoutDependencies.pipe( + buildServiceLayer(OrgResolver).pipe( Layer.provide(makeOrgMemberRepoLayer(opts.members ?? {})), Layer.provide(makeChannelRepoLayer(opts.channels ?? {})), Layer.provide(makeChannelMemberRepoLayer(opts.channelMembers ?? {})), @@ -70,18 +71,16 @@ const makeResolverLayer = (opts: { const runEither = ( make: Effect.Effect, - layer: Layer.Layer, + layer: Layer.Layer, actor: CurrentUser.Schema = makeActor(), ) => Effect.runPromise( - effect.pipe(Effect.provide(layer), Effect.provideService(CurrentUser.Context, actor), Effect.result), + make.pipe(Effect.provide(layer), Effect.provideService(CurrentUser.Context, actor), Effect.result), ) -const use = (fn: (resolver: OrgResolver) => Effect.Effect) => - Effect.gen(function* () { - const resolver = yield* OrgResolver - return yield* fn(resolver) - }) +const use = ( + fn: (resolver: ServiceMap.Service.Shape) => Effect.Effect, +) => serviceEffect(OrgResolver, fn) describe("OrgResolver", () => { describe("requireScope", () => { diff --git a/apps/backend/src/services/session-manager.ts b/apps/backend/src/services/session-manager.ts index 44efc653a..19687a240 100644 --- a/apps/backend/src/services/session-manager.ts +++ b/apps/backend/src/services/session-manager.ts @@ -1,4 +1,4 @@ -import { BackendAuth } from "@hazel/auth/backend" +import { BackendAuth, type UserRepoLike } from "@hazel/auth/backend" import { CurrentUser, InvalidBearerTokenError, @@ -18,13 +18,18 @@ export class SessionManager extends ServiceMap.Service()("Sessio make: Effect.gen(function* () { const auth = yield* BackendAuth const userRepo = yield* UserRepo + const userRepoLike: UserRepoLike = { + findByWorkOSUserId: userRepo.findByWorkOSUserId, + upsertWorkOSUser: userRepo.upsertWorkOSUser, + update: userRepo.update, + } /** * Authenticate with a WorkOS bearer token (JWT). * Verifies the JWT signature and syncs the user to the database. */ const authenticateWithBearer = (bearerToken: string) => - auth.authenticateWithBearer(bearerToken, userRepo) + auth.authenticateWithBearer(bearerToken, userRepoLike) return { authenticateWithBearer: authenticateWithBearer as ( diff --git a/apps/backend/src/test/effect-helpers.ts b/apps/backend/src/test/effect-helpers.ts index 63a08bcb6..7f72c9b85 100644 --- a/apps/backend/src/test/effect-helpers.ts +++ b/apps/backend/src/test/effect-helpers.ts @@ -1,16 +1,14 @@ import { ConfigProvider, Effect, Layer, ServiceMap } from "effect" -export const buildServiceLayer = < - T extends ServiceMap.Service.Any & { - readonly make: Effect.Effect +export const buildServiceLayer = ( + service: ServiceMap.Service & { + readonly make: Effect.Effect }, ->( - service: T, ) => Layer.effect(service, service.make) -export const serviceEffect = ( - service: T, - f: (implementation: ServiceMap.Service.Shape) => Effect.Effect, +export const serviceEffect = ( + service: ServiceMap.Service, + f: (implementation: S) => Effect.Effect, ) => service.use(f) export const serviceShape = ( diff --git a/apps/backend/src/test/message-outbox-repo.test.ts b/apps/backend/src/test/message-outbox-repo.test.ts index 32574963e..ccda68bb6 100644 --- a/apps/backend/src/test/message-outbox-repo.test.ts +++ b/apps/backend/src/test/message-outbox-repo.test.ts @@ -82,9 +82,9 @@ describe("MessageOutboxRepo", () => { ) expect(claimed).toHaveLength(3) - expect(claimed.map((event) => event.sequence)).toEqual( - [...claimed.map((event) => event.sequence)].sort((value, right) => left - right), - ) + expect(claimed.map((event) => event.sequence)).toEqual( + [...claimed.map((event) => event.sequence)].sort((left, right) => left - right), + ) expect(claimed.map((event) => event.eventType)).toEqual([ "message_created", "message_updated", diff --git a/apps/web/src/components/chat/channel-files/channel-files-documents-list.tsx b/apps/web/src/components/chat/channel-files/channel-files-documents-list.tsx index 87fa817cd..3e82d8b80 100644 --- a/apps/web/src/components/chat/channel-files/channel-files-documents-list.tsx +++ b/apps/web/src/components/chat/channel-files/channel-files-documents-list.tsx @@ -3,6 +3,7 @@ import { FileIcon } from "@untitledui/file-icons" import { IconDownload } from "~/components/icons/icon-download" import { Avatar } from "~/components/ui/avatar" import { Button } from "~/components/ui/button" +import { toDate } from "~/lib/utils" import { getAttachmentUrl } from "~/utils/attachment-url" import { formatFileSize, getFileTypeFromName } from "~/utils/file-utils" import { useChatAuthorIdentity } from "../author-identity" @@ -62,7 +63,7 @@ function DocumentItem({ attachment }: { attachment: AttachmentWithUser }) {
{formatFileSize(attachment.fileSize)} - {formatRelativeTime(attachment.uploadedAt)} + {formatRelativeTime(toDate(attachment.uploadedAt))}
diff --git a/apps/web/src/components/chat/channel-files/channel-files-media-gallery-view.tsx b/apps/web/src/components/chat/channel-files/channel-files-media-gallery-view.tsx index 5e8447a95..1680e1577 100644 --- a/apps/web/src/components/chat/channel-files/channel-files-media-gallery-view.tsx +++ b/apps/web/src/components/chat/channel-files/channel-files-media-gallery-view.tsx @@ -5,6 +5,7 @@ import { useState } from "react" import { IconDownload } from "~/components/icons/icon-download" import { Button } from "~/components/ui/button" import { useChannelAttachments } from "~/db/hooks" +import { toEpochMs } from "~/lib/utils" import { getAttachmentUrl } from "~/utils/attachment-url" import { getFileCategory, getFileTypeFromName } from "~/utils/file-utils" import { ImageViewerModal, type ViewerImage } from "../image-viewer-modal" @@ -181,7 +182,7 @@ export function MediaGalleryView({ channelId }: MediaGalleryViewProps) { images={viewerImages} initialIndex={selectedImageIndex} author={selectedImage?.user ?? undefined} - createdAt={selectedImage?.uploadedAt.getTime() ?? 0} + createdAt={selectedImage ? toEpochMs(selectedImage.uploadedAt) : 0} /> )} diff --git a/apps/web/src/components/chat/channel-files/channel-files-media-grid.tsx b/apps/web/src/components/chat/channel-files/channel-files-media-grid.tsx index 8ae884403..1004fdf93 100644 --- a/apps/web/src/components/chat/channel-files/channel-files-media-grid.tsx +++ b/apps/web/src/components/chat/channel-files/channel-files-media-grid.tsx @@ -6,6 +6,7 @@ import { useMemo, useState } from "react" import { IconDownload } from "~/components/icons/icon-download" import { Button } from "~/components/ui/button" import { useBreakpoint } from "~/hooks/use-breakpoint" +import { toEpochMs } from "~/lib/utils" import { getAttachmentUrl } from "~/utils/attachment-url" import { getFileTypeFromName } from "~/utils/file-utils" import { ImageViewerModal, type ViewerImage } from "../image-viewer-modal" @@ -217,7 +218,7 @@ export function ChannelFilesMediaGrid({ attachments, channelId }: ChannelFilesMe images={viewerImages} initialIndex={selectedIndex} author={selectedImage?.user ?? undefined} - createdAt={selectedImage?.uploadedAt.getTime() ?? 0} + createdAt={selectedImage ? toEpochMs(selectedImage.uploadedAt) : 0} /> )} diff --git a/apps/web/src/components/chat/inline-thread-preview.tsx b/apps/web/src/components/chat/inline-thread-preview.tsx index b8dce0c6f..baed425a1 100644 --- a/apps/web/src/components/chat/inline-thread-preview.tsx +++ b/apps/web/src/components/chat/inline-thread-preview.tsx @@ -4,6 +4,7 @@ import type { ChannelId, MessageId, UserId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { formatDistanceToNow } from "date-fns" import { useMemo } from "react" +import { toDate } from "~/lib/utils" import { threadMessageCountAtomFamily, userWithPresenceAtomFamily } from "~/atoms/message-atoms" import { channelCollection, messageCollection } from "~/db/collections" import { useChatStable } from "~/hooks/use-chat" @@ -106,7 +107,7 @@ export function InlineThreadPreview({ {lastReplyAt && ( {totalCount > 1 ? "Last reply " : ""} - {formatDistanceToNow(lastReplyAt, { addSuffix: false })} ago + {formatDistanceToNow(toDate(lastReplyAt), { addSuffix: false })} ago )} {/* View thread - absolutely positioned on hover */} diff --git a/apps/web/src/components/chat/message-attachments.tsx b/apps/web/src/components/chat/message-attachments.tsx index 9dc976db8..2101a7254 100644 --- a/apps/web/src/components/chat/message-attachments.tsx +++ b/apps/web/src/components/chat/message-attachments.tsx @@ -4,6 +4,7 @@ import { FileIcon } from "@untitledui/file-icons" import { useState } from "react" import { useAttachments, useMessage } from "~/db/hooks" +import { toEpochMs } from "~/lib/utils" import { getAttachmentUrl } from "~/utils/attachment-url" import { formatFileSize, getFileTypeFromName } from "~/utils/file-utils" import { IconDownload } from "../icons/icon-download" @@ -198,7 +199,7 @@ export function MessageAttachments({ messageId }: MessageAttachmentsProps) { images={viewerImages} initialIndex={selectedImageIndex} author={message.author} - createdAt={message.createdAt.getTime()} + createdAt={toEpochMs(message.createdAt)} /> ) })()} diff --git a/apps/web/src/components/chat/thread-panel.tsx b/apps/web/src/components/chat/thread-panel.tsx index 73455c2d4..384380688 100644 --- a/apps/web/src/components/chat/thread-panel.tsx +++ b/apps/web/src/components/chat/thread-panel.tsx @@ -2,6 +2,7 @@ import { useAtomSet } from "@effect/atom-react" import type { ChannelId, MessageId, OrganizationId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" import { format } from "date-fns" +import { toDate } from "~/lib/utils" import { generateThreadNameMutation } from "~/atoms/channel-atoms" import { exitToast } from "~/lib/toast-exit" import { channelCollection } from "~/db/collections" @@ -182,7 +183,7 @@ function ThreadContent({ {authorIdentity.displayName} - {format(originalMessage.createdAt, "MMM d, HH:mm")} + {format(toDate(originalMessage.createdAt), "MMM d, HH:mm")}
diff --git a/apps/web/src/components/command-palette/search-result-item.tsx b/apps/web/src/components/command-palette/search-result-item.tsx index 1b558ae69..c32574a86 100644 --- a/apps/web/src/components/command-palette/search-result-item.tsx +++ b/apps/web/src/components/command-palette/search-result-item.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef } from "react" import IconHashtag from "~/components/icons/icon-hashtag" import IconPaperclip from "~/components/icons/icon-paperclip2" import { Avatar } from "~/components/ui/avatar" -import { cn } from "~/lib/utils" +import { cn, toDate } from "~/lib/utils" import { MarkdownText } from "./markdown-text" interface SearchResultItemProps { @@ -92,7 +92,7 @@ export function SearchResultItem({ )} - {formatRelativeTime(message.createdAt)} + {formatRelativeTime(toDate(message.createdAt))}
diff --git a/apps/web/src/components/sidebar/channel-item.tsx b/apps/web/src/components/sidebar/channel-item.tsx index 8e2992fbd..3fa591839 100644 --- a/apps/web/src/components/sidebar/channel-item.tsx +++ b/apps/web/src/components/sidebar/channel-item.tsx @@ -1,5 +1,4 @@ import { useAtomSet } from "@effect/atom-react" -import type { Channel, ChannelMember } from "@hazel/db/schema" import type { ChannelSectionId } from "@hazel/schema" import { useNavigate } from "@tanstack/react-router" import { memo } from "react" @@ -25,13 +24,18 @@ import { usePermission } from "~/hooks/use-permission" import { useScrollIntoViewOnActive } from "~/hooks/use-scroll-into-view-on-active" import { exitToastAsync } from "~/lib/toast-exit" +import type { channelCollection, channelMemberCollection } from "~/db/collections" + +type ChannelData = (typeof channelCollection)["_type"] +type ChannelMemberData = (typeof channelMemberCollection)["_type"] + interface ChannelItemProps { - channel: Omit & { updatedAt: Date | null } - member: ChannelMember + channel: ChannelData + member: ChannelMemberData notificationCount?: number threads?: Array<{ - channel: Omit & { updatedAt: Date | null } - member: ChannelMember + channel: ChannelData + member: ChannelMemberData }> /** Available sections for "move to section" menu */ sections?: Array<{ id: ChannelSectionId; name: string }> diff --git a/apps/web/src/hooks/use-presence.ts b/apps/web/src/hooks/use-presence.ts index 6d2dd9fb6..cab348263 100644 --- a/apps/web/src/hooks/use-presence.ts +++ b/apps/web/src/hooks/use-presence.ts @@ -26,7 +26,7 @@ const ACTIVITY_BROADCAST_THROTTLE_MS = 1_000 */ const lastActivityAtom = Atom.make((get) => { let lastActivityMs = Date.now() - let lastActivity = DateTime.unsafeMake(new Date(lastActivityMs)) + let lastActivity = DateTime.makeUnsafe(new Date(lastActivityMs)) let lastBroadcastAtMs = 0 const tabId = @@ -60,14 +60,14 @@ const lastActivityAtom = Atom.make((get) => { const applyActivity = (at: number) => { if (!Number.isFinite(at) || at <= lastActivityMs) return lastActivityMs = at - lastActivity = DateTime.unsafeMake(new Date(at)) + lastActivity = DateTime.makeUnsafe(new Date(at)) get.setSelf(lastActivity) } const handleLocalActivity = () => { const at = Date.now() lastActivityMs = at - lastActivity = DateTime.unsafeMake(new Date(at)) + lastActivity = DateTime.makeUnsafe(new Date(at)) get.setSelf(lastActivity) if (at - lastBroadcastAtMs >= ACTIVITY_BROADCAST_THROTTLE_MS) { diff --git a/apps/web/src/lib/notifications/orchestrator.ts b/apps/web/src/lib/notifications/orchestrator.ts index 040b545bc..d68cf6868 100644 --- a/apps/web/src/lib/notifications/orchestrator.ts +++ b/apps/web/src/lib/notifications/orchestrator.ts @@ -1,3 +1,4 @@ +import { toEpochMs } from "~/lib/utils" import { pushNotificationDiagnostics } from "./diagnostics-store" import type { NotificationDecision, @@ -37,8 +38,8 @@ export class NotificationOrchestrator { } this.queue.sort((a, b) => { - const aTime = a.notification.createdAt.getTime() - const bTime = b.notification.createdAt.getTime() + const aTime = toEpochMs(a.notification.createdAt) + const bTime = toEpochMs(b.notification.createdAt) if (aTime !== bTime) return aTime - bTime return a.id.localeCompare(b.id) }) diff --git a/apps/web/src/routes/_app/$orgSlug/settings/invitations.tsx b/apps/web/src/routes/_app/$orgSlug/settings/invitations.tsx index cec2fcfa8..d71c79f70 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/invitations.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/invitations.tsx @@ -1,5 +1,6 @@ import { useAtomSet } from "@effect/atom-react" import type { InvitationId } from "@hazel/schema" +import { toEpochMs } from "~/lib/utils" import { IconArrowPath } from "~/components/icons/icon-arrow-path" import { eq, useLiveQuery } from "@tanstack/react-db" import { createFileRoute } from "@tanstack/react-router" @@ -222,7 +223,7 @@ function InvitationsSettings() {

{formatTimeRemaining( - invitation.expiresAt.getTime() - now, + toEpochMs(invitation.expiresAt) - now, )}

diff --git a/apps/web/src/utils/presence.ts b/apps/web/src/utils/presence.ts index a577687e9..043f2dedc 100644 --- a/apps/web/src/utils/presence.ts +++ b/apps/web/src/utils/presence.ts @@ -1,10 +1,12 @@ +import type { DateTime } from "effect" + export type PresenceStatus = "online" | "away" | "busy" | "dnd" | "offline" export const DEFAULT_OFFLINE_THRESHOLD_MS = 45_000 export type PresenceLike = { status?: string | null | undefined - lastSeenAt?: Date | null | undefined + lastSeenAt?: Date | DateTime.Utc | null | undefined } | null /** @@ -24,11 +26,16 @@ export function getEffectivePresenceStatus( if (!presence) return "offline" const lastSeenAt = presence.lastSeenAt - if (!(lastSeenAt instanceof Date) || Number.isNaN(lastSeenAt.getTime())) { + if (!lastSeenAt) { + return "offline" + } + + const lastSeenMs = lastSeenAt instanceof Date ? lastSeenAt.getTime() : lastSeenAt.epochMilliseconds + if (Number.isNaN(lastSeenMs)) { return "offline" } - if (nowMs - lastSeenAt.getTime() > offlineThresholdMs) { + if (nowMs - lastSeenMs > offlineThresholdMs) { return "offline" } diff --git a/packages/auth/src/consumers/backend-auth.ts b/packages/auth/src/consumers/backend-auth.ts index 48b810b2e..26c90d49f 100644 --- a/packages/auth/src/consumers/backend-auth.ts +++ b/packages/auth/src/consumers/backend-auth.ts @@ -72,7 +72,7 @@ export interface UserRepoLike { timezone: string | null settings: User.UserSettings | null }, - { _tag: "DatabaseError" } | { _tag: "ParseError" }, + { _tag: "DatabaseError" } | { _tag: "SchemaError" }, any > } @@ -236,7 +236,7 @@ export class BackendAuth extends ServiceMap.Service()("@hazel/auth/ detail: String(err), }), ), - ParseError: (err) => + SchemaError: (err) => Effect.fail( new SessionLoadError({ message: "Failed to parse user update response", From d108a1eee123d0e0b826286fd61fc3913a194054 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 16:26:35 +0100 Subject: [PATCH 24/34] fix patterns --- .../chat-sync/chat-sync-diagnostics.jsonl | 8 + apps/backend/src/lib/create-transactionId.ts | 4 +- .../src/policies/attachment-policy.test.ts | 46 +- apps/backend/src/policies/bot-policy.test.ts | 38 +- .../policies/channel-member-policy.test.ts | 44 +- .../src/policies/channel-policy.test.ts | 22 +- .../integration-connection-policy.test.ts | 12 +- .../src/policies/invitation-policy.test.ts | 41 +- .../src/policies/message-policy.test.ts | 38 +- .../policies/message-reaction-policy.test.ts | 28 +- .../src/policies/notification-policy.test.ts | 83 +- .../organization-member-policy.test.ts | 67 +- .../src/policies/organization-policy.test.ts | 27 +- .../policies/pinned-message-policy.test.ts | 83 +- .../src/policies/policy-test-helpers.ts | 26 +- .../policies/typing-indicator-policy.test.ts | 61 +- apps/backend/src/routes/auth.http.test.ts | 214 +- .../src/routes/bot-commands.http.test.ts | 20 +- apps/backend/src/routes/bot-commands.http.ts | 87 +- apps/backend/src/routes/bot-commands.sse.ts | 90 + apps/backend/src/routes/chat-sync.http.ts | 8 - .../src/routes/integration-resources.http.ts | 9 +- .../src/routes/integrations.http.test.ts | 2 +- apps/backend/src/routes/integrations.http.ts | 10 +- apps/backend/src/rpc/handlers/channels.ts | 17 +- .../src/rpc/handlers/connect-shares.test.ts | 8 +- .../backend/src/rpc/handlers/organizations.ts | 10 +- apps/backend/src/rpc/handlers/users.ts | 2 +- apps/backend/src/rpc/middleware/auth.test.ts | 490 ++-- .../src/services/bot-gateway-service.test.ts | 48 +- .../chat-sync-attachment-content.test.ts | 10 +- .../chat-sync-attribution-reconciler.test.ts | 49 +- .../chat-sync/chat-sync-core-worker.ts | 21 +- .../discord-gateway-service.dispatch.test.ts | 85 +- .../chat-sync/discord-gateway-service.test.ts | 2 +- .../chat-sync/discord-gateway-shared.ts | 545 ++++ .../chat-sync/discord-sync-worker.test.ts | 309 +- .../services/chat-sync/discord-sync-worker.ts | 9 +- .../connect-conversation-service.test.ts | 177 +- .../message-outbox-dispatcher.test.ts | 18 +- .../message-side-effect-service.test.ts | 206 +- .../backend/src/services/org-resolver.test.ts | 30 +- apps/backend/src/test/chat-sync-db-harness.ts | 183 +- apps/backend/src/test/effect-helpers.ts | 15 +- .../src/test/message-outbox-repo.test.ts | 22 +- apps/docs/src/routeTree.gen.ts | 104 +- apps/web/src/atoms/desktop-auth.ts | 477 ++-- apps/web/src/atoms/web-callback-atoms.ts | 97 +- .../channel-settings/integration-card.tsx | 3 +- .../channel-settings/openstatus-section.tsx | 3 +- .../channel-settings/railway-section.tsx | 3 +- .../connect/share-channel-modal.test.tsx | 12 +- .../openstatus-integration-content.tsx | 3 +- .../railway-integration-content.tsx | 3 +- .../rss-subscriptions-section.tsx | 3 +- apps/web/src/components/link-preview.tsx | 2 +- .../modals/create-organization-modal.tsx | 4 +- .../notifications/notification-item.tsx | 3 +- .../components/onboarding/org-setup-step.tsx | 12 +- .../src/components/sidebar/channel-item.tsx | 37 +- .../components/sidebar/channels-sidebar.tsx | 6 +- .../src/components/sidebar/thread-item.tsx | 15 +- apps/web/src/components/theme-provider.tsx | 55 +- apps/web/src/components/tweet-embed.tsx | 2 +- apps/web/src/db/actions.ts | 2 +- .../src/hooks/use-grouped-notifications.ts | 3 +- apps/web/src/hooks/use-typing.test.tsx | 36 +- apps/web/src/lib/auth-fetch.ts | 10 +- apps/web/src/lib/auth-token.ts | 91 +- apps/web/src/lib/connect-shared-channels.ts | 3 +- apps/web/src/lib/electric-fetch.ts | 6 +- apps/web/src/lib/error-messages.ts | 23 +- .../platform-storage/tauri-key-value-store.ts | 21 +- apps/web/src/lib/rpc-auth-middleware.ts | 6 +- .../web/src/lib/services/common/api-client.ts | 13 +- .../src/lib/services/common/network-mode.ts | 36 +- .../lib/services/common/rpc-atom-client.ts | 15 +- apps/web/src/lib/services/common/runtime.ts | 2 +- .../src/lib/services/desktop/tauri-auth.ts | 24 +- .../lib/services/desktop/token-exchange.ts | 52 +- apps/web/src/lib/toast-exit.tsx | 14 +- apps/web/src/routeTree.gen.ts | 2488 +++++++++-------- apps/web/src/routes/__root.tsx | 2 - .../channels/$channelId/settings/connect.tsx | 13 +- .../$channelId/settings/integrations.tsx | 3 +- .../settings/chat-sync/$connectionId.tsx | 3 +- .../$orgSlug/settings/chat-sync/index.tsx | 3 +- .../$orgSlug/settings/connect-invites.tsx | 7 +- .../_app/$orgSlug/settings/custom-emojis.tsx | 40 +- .../settings/integrations/$integrationId.tsx | 8 +- .../_app/onboarding/setup-organization.tsx | 4 +- apps/web/src/routes/auth/callback.tsx | 2 +- libs/bot-sdk/src/gateway.test.ts | 2 +- .../src/hazel-bot-sdk.error-handling.test.ts | 16 +- libs/bot-sdk/src/hazel-bot-sdk.test.ts | 15 +- libs/bot-sdk/src/hazel-bot-sdk.ts | 46 +- libs/bot-sdk/src/run-bot.ts | 8 +- .../src/streaming/streaming-service.ts | 4 +- libs/bot-sdk/src/streaming/types.ts | 4 +- .../src/AtomTanStackDB.result.test.ts | 6 +- .../src/AtomTanStackDB.subscription.test.ts | 6 +- .../src/AtomTanStackDB.test.ts | 4 +- .../src/AtomTanStackDB.timing.test.ts | 8 +- .../message-actor.create-conn-state.test.ts | 187 +- .../actors/src/auth/config-service.test.ts | 16 +- packages/actors/src/auth/config-service.ts | 2 +- packages/actors/src/auth/jwks-service.test.ts | 13 +- packages/actors/src/effect/runtime.ts | 8 +- .../auth/src/consumers/backend-auth.test.ts | 6 +- packages/auth/src/consumers/backend-auth.ts | 2 +- .../src/services/workos-sync.test.ts | 4 +- packages/domain/src/rpc/channels.ts | 7 +- vitest.config.ts | 10 +- 113 files changed, 4358 insertions(+), 3114 deletions(-) create mode 100644 apps/backend/.artifacts/chat-sync/chat-sync-diagnostics.jsonl create mode 100644 apps/backend/src/routes/bot-commands.sse.ts create mode 100644 apps/backend/src/services/chat-sync/discord-gateway-shared.ts diff --git a/apps/backend/.artifacts/chat-sync/chat-sync-diagnostics.jsonl b/apps/backend/.artifacts/chat-sync/chat-sync-diagnostics.jsonl new file mode 100644 index 000000000..7750b942d --- /dev/null +++ b/apps/backend/.artifacts/chat-sync/chat-sync-diagnostics.jsonl @@ -0,0 +1,8 @@ +{"timestamp":"2026-03-16T14:12:48.118Z","suite":"discord-gateway-dispatch","testCase":"message-create-routing","workerMethod":"ingestMessageCreate","action":"dispatch","dedupeKey":"discord:gateway:create:223456789012345678","syncConnectionId":"00000000-0000-0000-0000-000000000001","expected":"one inbound dispatch for both-direction link","actual":"1 dispatches"} +{"timestamp":"2026-03-16T14:39:47.254Z","suite":"discord-gateway-dispatch","testCase":"message-create-routing","workerMethod":"ingestMessageCreate","action":"dispatch","dedupeKey":"discord:gateway:create:223456789012345678","syncConnectionId":"00000000-0000-4000-8000-000000000001","expected":"one inbound dispatch for both-direction link","actual":"1 dispatches"} +{"timestamp":"2026-03-16T14:39:52.824Z","suite":"discord-gateway-dispatch","testCase":"message-create-routing","workerMethod":"ingestMessageCreate","action":"dispatch","dedupeKey":"discord:gateway:create:223456789012345678","syncConnectionId":"00000000-0000-4000-8000-000000000001","expected":"one inbound dispatch for both-direction link","actual":"1 dispatches"} +{"timestamp":"2026-03-16T14:39:57.213Z","suite":"chat-sync-core-worker.integration","testCase":"hazel-message-create-update-delete","workerMethod":"syncHazelMessageToProvider","action":"create","syncConnectionId":"4921e0de-a39a-4490-9568-c7f81d1fa558","expected":"synced","actual":"synced"} +{"timestamp":"2026-03-16T14:39:57.997Z","suite":"chat-sync-core-worker.integration","testCase":"direction-inactive-webhook-origin-guards","workerMethod":"syncHazelMessageCreateToAllConnections","action":"direction_gate","expected":"1 synced","actual":"1 synced"} +{"timestamp":"2026-03-16T14:48:20.247Z","suite":"discord-gateway-dispatch","testCase":"message-create-routing","workerMethod":"ingestMessageCreate","action":"dispatch","dedupeKey":"discord:gateway:create:223456789012345678","syncConnectionId":"00000000-0000-4000-8000-000000000001","expected":"one inbound dispatch for both-direction link","actual":"1 dispatches"} +{"timestamp":"2026-03-16T14:48:28.056Z","suite":"chat-sync-core-worker.integration","testCase":"hazel-message-create-update-delete","workerMethod":"syncHazelMessageToProvider","action":"create","syncConnectionId":"58d534a7-35af-470d-8a28-89220a0988a3","expected":"synced","actual":"synced"} +{"timestamp":"2026-03-16T14:48:30.436Z","suite":"chat-sync-core-worker.integration","testCase":"direction-inactive-webhook-origin-guards","workerMethod":"syncHazelMessageCreateToAllConnections","action":"direction_gate","expected":"1 synced","actual":"1 synced"} diff --git a/apps/backend/src/lib/create-transactionId.ts b/apps/backend/src/lib/create-transactionId.ts index 8aed731d2..b5af8b6d7 100644 --- a/apps/backend/src/lib/create-transactionId.ts +++ b/apps/backend/src/lib/create-transactionId.ts @@ -1,6 +1,6 @@ import { Database } from "@hazel/db" import { TransactionIdFromString } from "@hazel/schema" -import { Effect, Option, Predicate, Schema, SchemaIssue } from "effect" +import { Effect, Option, Predicate, Schema } from "effect" export const generateTransactionId = Effect.fn("generateTransactionId")(function* ( tx?: ( @@ -38,7 +38,7 @@ export const generateTransactionId = Effect.fn("generateTransactionId")(function (e): e is Database.DatabaseError => Predicate.isTagged(e, "DatabaseError"), (err) => Effect.die(`Database error generating transaction ID: ${err}`), ), - Effect.catchIf(SchemaIssue.isIssue, (err) => Effect.die(`Failed to parse transaction ID: ${err}`)), + Effect.catchTag("SchemaError", (err) => Effect.die(`Failed to parse transaction ID: ${err}`)), ) return result diff --git a/apps/backend/src/policies/attachment-policy.test.ts b/apps/backend/src/policies/attachment-policy.test.ts index 670e23d34..fdc5335e1 100644 --- a/apps/backend/src/policies/attachment-policy.test.ts +++ b/apps/backend/src/policies/attachment-policy.test.ts @@ -4,13 +4,11 @@ import type { AttachmentId, ChannelId, MessageId, OrganizationId, UserId } from import { Effect, Result, Layer, Option } from "effect" import { AttachmentPolicy } from "./attachment-policy.ts" import { - buildServiceLayer, makeActor, makeEntityNotFound, makeOrganizationMemberRepoLayer, makeOrgResolverLayer, runWithActorEither, - serviceEffect, serviceShape, TEST_ORG_ID, TEST_USER_ID, @@ -18,12 +16,12 @@ import { type Role = "admin" | "member" | "owner" -const ATTACHMENT_ID = "00000000-0000-0000-0000-000000000831" as AttachmentId -const MESSAGE_ID = "00000000-0000-0000-0000-000000000832" as MessageId -const CHANNEL_ID = "00000000-0000-0000-0000-000000000833" as ChannelId -const OTHER_USER_ID = "00000000-0000-0000-0000-000000000834" as UserId -const ADMIN_USER_ID = "00000000-0000-0000-0000-000000000835" as UserId -const MESSAGE_AUTHOR_ID = "00000000-0000-0000-0000-000000000836" as UserId +const ATTACHMENT_ID = "00000000-0000-4000-8000-000000000831" as AttachmentId +const MESSAGE_ID = "00000000-0000-4000-8000-000000000832" as MessageId +const CHANNEL_ID = "00000000-0000-4000-8000-000000000833" as ChannelId +const OTHER_USER_ID = "00000000-0000-4000-8000-000000000834" as UserId +const ADMIN_USER_ID = "00000000-0000-4000-8000-000000000835" as UserId +const MESSAGE_AUTHOR_ID = "00000000-0000-4000-8000-000000000836" as UserId const makeAttachmentRepoLayer = ( attachments: Record, @@ -90,7 +88,7 @@ const makePolicyLayer = (opts: { channels?: Record channelMemberships?: Record }) => - buildServiceLayer(AttachmentPolicy).pipe( + Layer.effect(AttachmentPolicy, AttachmentPolicy.make).pipe( Layer.provide(makeAttachmentRepoLayer(opts.attachments ?? {})), Layer.provide(makeMessageRepoLayer(opts.messages ?? {})), Layer.provide(makeChannelRepoLayer(opts.channels ?? {})), @@ -105,7 +103,7 @@ describe("AttachmentPolicy", () => { const layer = makePolicyLayer({}) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canCreate()), + AttachmentPolicy.use((policy) => policy.canCreate()), layer, actor, ) @@ -121,7 +119,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canUpdate(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canUpdate(ATTACHMENT_ID)), layer, actor, ) @@ -137,7 +135,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canUpdate(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canUpdate(ATTACHMENT_ID)), layer, actor, ) @@ -153,7 +151,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canDelete(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canDelete(ATTACHMENT_ID)), layer, actor, ) @@ -169,7 +167,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canDelete(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canDelete(ATTACHMENT_ID)), layer, actor, ) @@ -191,7 +189,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canDelete(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canDelete(ATTACHMENT_ID)), layer, actor, ) @@ -213,7 +211,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canDelete(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canDelete(ATTACHMENT_ID)), layer, actor, ) @@ -238,7 +236,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canDelete(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canDelete(ATTACHMENT_ID)), layer, actor, ) @@ -263,7 +261,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canDelete(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canDelete(ATTACHMENT_ID)), layer, actor, ) @@ -279,7 +277,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canView(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canView(ATTACHMENT_ID)), layer, actor, ) @@ -295,7 +293,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canView(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canView(ATTACHMENT_ID)), layer, actor, ) @@ -320,7 +318,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canView(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canView(ATTACHMENT_ID)), layer, actor, ) @@ -345,7 +343,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canView(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canView(ATTACHMENT_ID)), layer, actor, ) @@ -370,7 +368,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canView(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canView(ATTACHMENT_ID)), layer, actor, ) @@ -395,7 +393,7 @@ describe("AttachmentPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(AttachmentPolicy, (policy) => policy.canView(ATTACHMENT_ID)), + AttachmentPolicy.use((policy) => policy.canView(ATTACHMENT_ID)), layer, actor, ) diff --git a/apps/backend/src/policies/bot-policy.test.ts b/apps/backend/src/policies/bot-policy.test.ts index 6f0731d61..6b543bd13 100644 --- a/apps/backend/src/policies/bot-policy.test.ts +++ b/apps/backend/src/policies/bot-policy.test.ts @@ -5,20 +5,18 @@ import type { BotId, UserId } from "@hazel/schema" import { Effect, Result, Layer, ServiceMap } from "effect" import { BotPolicy } from "./bot-policy.ts" import { - buildServiceLayer, makeActor, makeEntityNotFound, makeOrgResolverLayer, runWithActorEither, - serviceEffect, TEST_ALT_ORG_ID, TEST_ORG_ID, } from "./policy-test-helpers.ts" type Role = "admin" | "member" | "owner" -const BOT_ID = "00000000-0000-0000-0000-000000000401" as BotId -const MISSING_BOT_ID = "00000000-0000-0000-0000-000000000499" as BotId +const BOT_ID = "00000000-0000-4000-8000-000000000401" as BotId +const MISSING_BOT_ID = "00000000-0000-4000-8000-000000000499" as BotId const makeBotRepoLayer = (bots: Record) => Layer.succeed(BotRepo, { @@ -32,7 +30,7 @@ const makeBotRepoLayer = (bots: Record) => } as ServiceMap.Service.Shape) const makePolicyLayer = (members: Record, bots: Record) => - buildServiceLayer(BotPolicy).pipe( + Layer.effect(BotPolicy, BotPolicy.make).pipe( Layer.provide(makeOrgResolverLayer(members)), Layer.provide(makeBotRepoLayer(bots)), ) @@ -48,12 +46,12 @@ describe("BotPolicy", () => { ) const allowed = await runWithActorEither( - serviceEffect(BotPolicy, (policy) => policy.canCreate(TEST_ORG_ID)), + BotPolicy.use((policy) => policy.canCreate(TEST_ORG_ID)), layer, actor, ) const denied = await runWithActorEither( - serviceEffect(BotPolicy, (policy) => policy.canCreate(TEST_ALT_ORG_ID)), + BotPolicy.use((policy) => policy.canCreate(TEST_ALT_ORG_ID)), layer, actor, ) @@ -65,10 +63,10 @@ describe("BotPolicy", () => { it("canRead allows creator or org admin", async () => { const creator = makeActor() const admin = makeActor({ - id: "00000000-0000-0000-0000-000000000402" as UserId, + id: "00000000-0000-4000-8000-000000000402" as UserId, }) const outsider = makeActor({ - id: "00000000-0000-0000-0000-000000000403" as UserId, + id: "00000000-0000-4000-8000-000000000403" as UserId, organizationId: TEST_ORG_ID, }) @@ -83,17 +81,17 @@ describe("BotPolicy", () => { ) const creatorAllowed = await runWithActorEither( - serviceEffect(BotPolicy, (policy) => policy.canRead(BOT_ID)), + BotPolicy.use((policy) => policy.canRead(BOT_ID)), layer, creator, ) const adminAllowed = await runWithActorEither( - serviceEffect(BotPolicy, (policy) => policy.canRead(BOT_ID)), + BotPolicy.use((policy) => policy.canRead(BOT_ID)), layer, makeActor({ ...admin, organizationId: TEST_ORG_ID }), ) const outsiderDenied = await runWithActorEither( - serviceEffect(BotPolicy, (policy) => policy.canRead(BOT_ID)), + BotPolicy.use((policy) => policy.canRead(BOT_ID)), layer, outsider, ) @@ -106,22 +104,22 @@ describe("BotPolicy", () => { it("canUpdate/canDelete require creator and map missing bot to UnauthorizedError", async () => { const creator = makeActor() const otherUser = makeActor({ - id: "00000000-0000-0000-0000-000000000404" as UserId, + id: "00000000-0000-4000-8000-000000000404" as UserId, }) const layer = makePolicyLayer({}, { [BOT_ID]: { createdBy: creator.id } }) const updateCreator = await runWithActorEither( - serviceEffect(BotPolicy, (policy) => policy.canUpdate(BOT_ID)), + BotPolicy.use((policy) => policy.canUpdate(BOT_ID)), layer, creator, ) const updateOther = await runWithActorEither( - serviceEffect(BotPolicy, (policy) => policy.canUpdate(BOT_ID)), + BotPolicy.use((policy) => policy.canUpdate(BOT_ID)), layer, otherUser, ) const deleteMissing = await runWithActorEither( - serviceEffect(BotPolicy, (policy) => policy.canDelete(MISSING_BOT_ID)), + BotPolicy.use((policy) => policy.canDelete(MISSING_BOT_ID)), layer, creator, ) @@ -137,7 +135,7 @@ describe("BotPolicy", () => { it("canInstall and canUninstall require admin-or-owner", async () => { const admin = makeActor() const member = makeActor({ - id: "00000000-0000-0000-0000-000000000405" as UserId, + id: "00000000-0000-4000-8000-000000000405" as UserId, }) const layer = makePolicyLayer( { @@ -148,17 +146,17 @@ describe("BotPolicy", () => { ) const installAdmin = await runWithActorEither( - serviceEffect(BotPolicy, (policy) => policy.canInstall(TEST_ORG_ID)), + BotPolicy.use((policy) => policy.canInstall(TEST_ORG_ID)), layer, admin, ) const uninstallAdmin = await runWithActorEither( - serviceEffect(BotPolicy, (policy) => policy.canUninstall(TEST_ORG_ID)), + BotPolicy.use((policy) => policy.canUninstall(TEST_ORG_ID)), layer, admin, ) const installMember = await runWithActorEither( - serviceEffect(BotPolicy, (policy) => policy.canInstall(TEST_ORG_ID)), + BotPolicy.use((policy) => policy.canInstall(TEST_ORG_ID)), layer, member, ) diff --git a/apps/backend/src/policies/channel-member-policy.test.ts b/apps/backend/src/policies/channel-member-policy.test.ts index 587662106..b9dd6535a 100644 --- a/apps/backend/src/policies/channel-member-policy.test.ts +++ b/apps/backend/src/policies/channel-member-policy.test.ts @@ -5,13 +5,11 @@ import type { ChannelId, ChannelMemberId, OrganizationId, UserId } from "@hazel/ import { Effect, Result, Layer, Option } from "effect" import { ChannelMemberPolicy } from "./channel-member-policy.ts" import { - buildServiceLayer, makeActor, makeEntityNotFound, makeOrganizationMemberRepoLayer, makeOrgResolverLayer, runWithActorEither, - serviceEffect, serviceShape, TEST_ORG_ID, TEST_USER_ID, @@ -19,11 +17,11 @@ import { type Role = "admin" | "member" | "owner" -const CHANNEL_ID = "00000000-0000-0000-0000-000000000811" as ChannelId -const CHANNEL_MEMBER_ID = "00000000-0000-0000-0000-000000000812" as ChannelMemberId -const MISSING_CHANNEL_MEMBER_ID = "00000000-0000-0000-0000-000000000819" as ChannelMemberId -const ADMIN_USER_ID = "00000000-0000-0000-0000-000000000813" as UserId -const OWNER_USER_ID = "00000000-0000-0000-0000-000000000814" as UserId +const CHANNEL_ID = "00000000-0000-4000-8000-000000000811" as ChannelId +const CHANNEL_MEMBER_ID = "00000000-0000-4000-8000-000000000812" as ChannelMemberId +const MISSING_CHANNEL_MEMBER_ID = "00000000-0000-4000-8000-000000000819" as ChannelMemberId +const ADMIN_USER_ID = "00000000-0000-4000-8000-000000000813" as UserId +const OWNER_USER_ID = "00000000-0000-4000-8000-000000000814" as UserId interface ChannelMemberEntry { userId: UserId @@ -71,7 +69,7 @@ const makePolicyLayer = (opts: { channelMembers?: Record membershipsByChannelAndUser?: Record }) => - buildServiceLayer(ChannelMemberPolicy).pipe( + Layer.effect(ChannelMemberPolicy, ChannelMemberPolicy.make).pipe( Layer.provide( makeChannelMemberRepoLayer(opts.channelMembers ?? {}, opts.membershipsByChannelAndUser ?? {}), ), @@ -92,7 +90,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.isOwner(CHANNEL_MEMBER_ID)), + ChannelMemberPolicy.use((policy) => policy.isOwner(CHANNEL_MEMBER_ID)), layer, actor, ) @@ -110,7 +108,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.isOwner(CHANNEL_MEMBER_ID)), + ChannelMemberPolicy.use((policy) => policy.isOwner(CHANNEL_MEMBER_ID)), layer, actor, ) @@ -135,12 +133,12 @@ describe("ChannelMemberPolicy", () => { }) const adminResult = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canCreate(CHANNEL_ID)), + ChannelMemberPolicy.use((policy) => policy.canCreate(CHANNEL_ID)), layer, admin, ) const ownerResult = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canCreate(CHANNEL_ID)), + ChannelMemberPolicy.use((policy) => policy.canCreate(CHANNEL_ID)), layer, owner, ) @@ -161,7 +159,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canCreate(CHANNEL_ID)), + ChannelMemberPolicy.use((policy) => policy.canCreate(CHANNEL_ID)), layer, actor, ) @@ -180,7 +178,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canCreate(CHANNEL_ID)), + ChannelMemberPolicy.use((policy) => policy.canCreate(CHANNEL_ID)), layer, actor, ) @@ -205,7 +203,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canRead(CHANNEL_ID)), + ChannelMemberPolicy.use((policy) => policy.canRead(CHANNEL_ID)), layer, actor, ) @@ -225,7 +223,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canRead(CHANNEL_ID)), + ChannelMemberPolicy.use((policy) => policy.canRead(CHANNEL_ID)), layer, admin, ) @@ -245,7 +243,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canRead(CHANNEL_ID)), + ChannelMemberPolicy.use((policy) => policy.canRead(CHANNEL_ID)), layer, actor, ) @@ -270,7 +268,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canUpdate(CHANNEL_MEMBER_ID)), + ChannelMemberPolicy.use((policy) => policy.canUpdate(CHANNEL_MEMBER_ID)), layer, actor, ) @@ -296,12 +294,12 @@ describe("ChannelMemberPolicy", () => { }) const adminResult = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canUpdate(CHANNEL_MEMBER_ID)), + ChannelMemberPolicy.use((policy) => policy.canUpdate(CHANNEL_MEMBER_ID)), layer, admin, ) const ownerResult = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canUpdate(CHANNEL_MEMBER_ID)), + ChannelMemberPolicy.use((policy) => policy.canUpdate(CHANNEL_MEMBER_ID)), layer, owner, ) @@ -330,7 +328,7 @@ describe("ChannelMemberPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canDelete(CHANNEL_MEMBER_ID)), + ChannelMemberPolicy.use((policy) => policy.canDelete(CHANNEL_MEMBER_ID)), layer, actor, ) @@ -356,12 +354,12 @@ describe("ChannelMemberPolicy", () => { }) const adminResult = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canDelete(CHANNEL_MEMBER_ID)), + ChannelMemberPolicy.use((policy) => policy.canDelete(CHANNEL_MEMBER_ID)), layer, admin, ) const ownerResult = await runWithActorEither( - serviceEffect(ChannelMemberPolicy, (policy) => policy.canDelete(CHANNEL_MEMBER_ID)), + ChannelMemberPolicy.use((policy) => policy.canDelete(CHANNEL_MEMBER_ID)), layer, owner, ) diff --git a/apps/backend/src/policies/channel-policy.test.ts b/apps/backend/src/policies/channel-policy.test.ts index 91f8efdf3..de3b12811 100644 --- a/apps/backend/src/policies/channel-policy.test.ts +++ b/apps/backend/src/policies/channel-policy.test.ts @@ -5,20 +5,18 @@ import type { ChannelId, OrganizationId } from "@hazel/schema" import { Effect, Result, Layer, ServiceMap } from "effect" import { ChannelPolicy } from "./channel-policy.ts" import { - buildServiceLayer, makeActor, makeEntityNotFound, makeOrgResolverLayer, runWithActorEither, - serviceEffect, TEST_ALT_ORG_ID, TEST_ORG_ID, } from "./policy-test-helpers.ts" type Role = "admin" | "member" | "owner" -const CHANNEL_ID = "00000000-0000-0000-0000-000000000301" as ChannelId -const MISSING_CHANNEL_ID = "00000000-0000-0000-0000-000000000399" as ChannelId +const CHANNEL_ID = "00000000-0000-4000-8000-000000000301" as ChannelId +const MISSING_CHANNEL_ID = "00000000-0000-4000-8000-000000000399" as ChannelId const makeChannelRepoLayer = (channels: Record) => Layer.succeed(ChannelRepo, { @@ -38,7 +36,7 @@ const makePolicyLayer = ( members: Record, channels: Record, ) => - buildServiceLayer(ChannelPolicy).pipe( + Layer.effect(ChannelPolicy, ChannelPolicy.make).pipe( Layer.provide(makeChannelRepoLayer(channels)), Layer.provide(makeOrgResolverLayer(members)), ) @@ -52,25 +50,25 @@ describe("ChannelPolicy", () => { const ownerLayer = makePolicyLayer({ [`${TEST_ORG_ID}:${actor.id}`]: "owner" }, {}) const memberResult = await runWithActorEither( - serviceEffect(ChannelPolicy, (policy) => policy.canCreate(TEST_ORG_ID)), + ChannelPolicy.use((policy) => policy.canCreate(TEST_ORG_ID)), memberLayer, actor, ["channels:write"], ) const adminResult = await runWithActorEither( - serviceEffect(ChannelPolicy, (policy) => policy.canCreate(TEST_ORG_ID)), + ChannelPolicy.use((policy) => policy.canCreate(TEST_ORG_ID)), adminLayer, actor, ["channels:write"], ) const ownerResult = await runWithActorEither( - serviceEffect(ChannelPolicy, (policy) => policy.canCreate(TEST_ORG_ID)), + ChannelPolicy.use((policy) => policy.canCreate(TEST_ORG_ID)), ownerLayer, actor, ["channels:write"], ) const noMembership = await runWithActorEither( - serviceEffect(ChannelPolicy, (policy) => policy.canCreate(TEST_ALT_ORG_ID)), + ChannelPolicy.use((policy) => policy.canCreate(TEST_ALT_ORG_ID)), memberLayer, actor, ["channels:write"], @@ -94,12 +92,12 @@ describe("ChannelPolicy", () => { ) const allowed = await runWithActorEither( - serviceEffect(ChannelPolicy, (policy) => policy.canUpdate(CHANNEL_ID)), + ChannelPolicy.use((policy) => policy.canUpdate(CHANNEL_ID)), layer, actor, ) const missing = await runWithActorEither( - serviceEffect(ChannelPolicy, (policy) => policy.canUpdate(MISSING_CHANNEL_ID)), + ChannelPolicy.use((policy) => policy.canUpdate(MISSING_CHANNEL_ID)), layer, actor, ) @@ -123,7 +121,7 @@ describe("ChannelPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(ChannelPolicy, (policy) => policy.canDelete(CHANNEL_ID)), + ChannelPolicy.use((policy) => policy.canDelete(CHANNEL_ID)), layer, actor, ) diff --git a/apps/backend/src/policies/integration-connection-policy.test.ts b/apps/backend/src/policies/integration-connection-policy.test.ts index aea92527d..69729d788 100644 --- a/apps/backend/src/policies/integration-connection-policy.test.ts +++ b/apps/backend/src/policies/integration-connection-policy.test.ts @@ -3,18 +3,16 @@ import { UnauthorizedError } from "@hazel/domain" import { Result, Layer } from "effect" import { IntegrationConnectionPolicy } from "./integration-connection-policy.ts" import { - buildServiceLayer, makeActor, makeOrgResolverLayer, runWithActorEither, - serviceEffect, TEST_ORG_ID, } from "./policy-test-helpers.ts" type Role = "admin" | "member" | "owner" const makePolicyLayer = (members: Record) => - buildServiceLayer(IntegrationConnectionPolicy).pipe(Layer.provide(makeOrgResolverLayer(members))) + Layer.effect(IntegrationConnectionPolicy, IntegrationConnectionPolicy.make).pipe(Layer.provide(makeOrgResolverLayer(members))) describe("IntegrationConnectionPolicy", () => { it("allows select for any org member", async () => { @@ -24,7 +22,7 @@ describe("IntegrationConnectionPolicy", () => { }) const result = await runWithActorEither( - serviceEffect(IntegrationConnectionPolicy, (policy) => policy.canSelect(TEST_ORG_ID)), + IntegrationConnectionPolicy.use((policy) => policy.canSelect(TEST_ORG_ID)), layer, actor, ) @@ -38,17 +36,17 @@ describe("IntegrationConnectionPolicy", () => { }) const insert = await runWithActorEither( - serviceEffect(IntegrationConnectionPolicy, (policy) => policy.canInsert(TEST_ORG_ID)), + IntegrationConnectionPolicy.use((policy) => policy.canInsert(TEST_ORG_ID)), layer, actor, ) const update = await runWithActorEither( - serviceEffect(IntegrationConnectionPolicy, (policy) => policy.canUpdate(TEST_ORG_ID)), + IntegrationConnectionPolicy.use((policy) => policy.canUpdate(TEST_ORG_ID)), layer, actor, ) const del = await runWithActorEither( - serviceEffect(IntegrationConnectionPolicy, (policy) => policy.canDelete(TEST_ORG_ID)), + IntegrationConnectionPolicy.use((policy) => policy.canDelete(TEST_ORG_ID)), layer, actor, ) diff --git a/apps/backend/src/policies/invitation-policy.test.ts b/apps/backend/src/policies/invitation-policy.test.ts index e28651f7d..3c3c97582 100644 --- a/apps/backend/src/policies/invitation-policy.test.ts +++ b/apps/backend/src/policies/invitation-policy.test.ts @@ -5,22 +5,21 @@ import type { InvitationId, OrganizationId, UserId } from "@hazel/schema" import { Effect, Result, Layer, Option, ServiceMap } from "effect" import { InvitationPolicy } from "./invitation-policy.ts" import { - buildServiceLayer, makeActor, makeEntityNotFound, makeOrganizationMemberRepoLayer, makeOrgResolverLayer, runWithActorEither, - serviceEffect, + serviceShape, TEST_ORG_ID, TEST_USER_ID, } from "./policy-test-helpers.ts" type Role = "admin" | "member" | "owner" -const INVITATION_ID = "00000000-0000-0000-0000-000000000821" as InvitationId -const MISSING_INVITATION_ID = "00000000-0000-0000-0000-000000000829" as InvitationId -const ADMIN_USER_ID = "00000000-0000-0000-0000-000000000822" as UserId +const INVITATION_ID = "00000000-0000-4000-8000-000000000821" as InvitationId +const MISSING_INVITATION_ID = "00000000-0000-4000-8000-000000000829" as InvitationId +const ADMIN_USER_ID = "00000000-0000-4000-8000-000000000822" as UserId const makeInvitationRepoLayer = ( invitations: Record, @@ -43,19 +42,19 @@ const makeInvitationRepoLayer = ( } as ServiceMap.Service.Shape) const makeUserRepoLayer = (users: Record) => - Layer.succeed(UserRepo, { + Layer.succeed(UserRepo, serviceShape({ findById: (id: UserId) => { const user = users[id] return Effect.succeed(user ? Option.some(user) : Option.none()) }, - } as ServiceMap.Service.Shape) + })) const makePolicyLayer = ( members: Record, invitations: Record, users: Record = {}, ) => - buildServiceLayer(InvitationPolicy).pipe( + Layer.effect(InvitationPolicy, InvitationPolicy.make).pipe( Layer.provide(makeOrgResolverLayer(members)), Layer.provide(makeOrganizationMemberRepoLayer(members)), Layer.provide(makeInvitationRepoLayer(invitations)), @@ -68,7 +67,7 @@ describe("InvitationPolicy", () => { const layer = makePolicyLayer({}, {}) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canRead(INVITATION_ID)), + InvitationPolicy.use((policy) => policy.canRead(INVITATION_ID)), layer, actor, ) @@ -86,7 +85,7 @@ describe("InvitationPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canCreate(TEST_ORG_ID)), + InvitationPolicy.use((policy) => policy.canCreate(TEST_ORG_ID)), layer, actor, ) @@ -104,7 +103,7 @@ describe("InvitationPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canCreate(TEST_ORG_ID)), + InvitationPolicy.use((policy) => policy.canCreate(TEST_ORG_ID)), layer, actor, ) @@ -126,7 +125,7 @@ describe("InvitationPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canUpdate(INVITATION_ID)), + InvitationPolicy.use((policy) => policy.canUpdate(INVITATION_ID)), layer, actor, ) @@ -150,7 +149,7 @@ describe("InvitationPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canUpdate(INVITATION_ID)), + InvitationPolicy.use((policy) => policy.canUpdate(INVITATION_ID)), layer, admin, ) @@ -174,7 +173,7 @@ describe("InvitationPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canUpdate(INVITATION_ID)), + InvitationPolicy.use((policy) => policy.canUpdate(INVITATION_ID)), layer, outsider, ) @@ -196,7 +195,7 @@ describe("InvitationPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canDelete(INVITATION_ID)), + InvitationPolicy.use((policy) => policy.canDelete(INVITATION_ID)), layer, actor, ) @@ -220,7 +219,7 @@ describe("InvitationPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canDelete(INVITATION_ID)), + InvitationPolicy.use((policy) => policy.canDelete(INVITATION_ID)), layer, admin, ) @@ -245,7 +244,7 @@ describe("InvitationPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canAccept(INVITATION_ID)), + InvitationPolicy.use((policy) => policy.canAccept(INVITATION_ID)), layer, actor, ) @@ -270,7 +269,7 @@ describe("InvitationPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canAccept(INVITATION_ID)), + InvitationPolicy.use((policy) => policy.canAccept(INVITATION_ID)), layer, actor, ) @@ -293,7 +292,7 @@ describe("InvitationPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canAccept(INVITATION_ID)), + InvitationPolicy.use((policy) => policy.canAccept(INVITATION_ID)), layer, actor, ) @@ -311,7 +310,7 @@ describe("InvitationPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canList(TEST_ORG_ID)), + InvitationPolicy.use((policy) => policy.canList(TEST_ORG_ID)), layer, actor, ) @@ -329,7 +328,7 @@ describe("InvitationPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(InvitationPolicy, (policy) => policy.canList(TEST_ORG_ID)), + InvitationPolicy.use((policy) => policy.canList(TEST_ORG_ID)), layer, actor, ) diff --git a/apps/backend/src/policies/message-policy.test.ts b/apps/backend/src/policies/message-policy.test.ts index 7bec2e29b..921fa4e32 100644 --- a/apps/backend/src/policies/message-policy.test.ts +++ b/apps/backend/src/policies/message-policy.test.ts @@ -6,12 +6,10 @@ import { Effect, Result, Layer, Option } from "effect" import { OrgResolver } from "../services/org-resolver.ts" import { MessagePolicy } from "./message-policy.ts" import { - buildServiceLayer, makeActor, makeEntityNotFound, makeOrganizationMemberRepoLayer, runWithActorEither, - serviceEffect, serviceShape, TEST_ORG_ID, TEST_USER_ID, @@ -19,9 +17,9 @@ import { type Role = "admin" | "member" | "owner" -const CHANNEL_ID = "00000000-0000-0000-0000-000000000801" as ChannelId -const MESSAGE_ID = "00000000-0000-0000-0000-000000000802" as MessageId -const MISSING_MESSAGE_ID = "00000000-0000-0000-0000-000000000899" as MessageId +const CHANNEL_ID = "00000000-0000-4000-8000-000000000801" as ChannelId +const MESSAGE_ID = "00000000-0000-4000-8000-000000000802" as MessageId +const MISSING_MESSAGE_ID = "00000000-0000-4000-8000-000000000899" as MessageId /** * Creates a ChannelRepo mock with both `findById` (for OrgResolver) and `with` (for MessagePolicy). @@ -88,14 +86,14 @@ const makePolicyLayer = ( const messageRepoLayer = makeMessageRepoLayer(messages) const orgMemberRepoLayer = makeOrganizationMemberRepoLayer(members) - const orgResolverLayer = buildServiceLayer(OrgResolver).pipe( + const orgResolverLayer = Layer.effect(OrgResolver, OrgResolver.make).pipe( Layer.provide(orgMemberRepoLayer), Layer.provide(channelRepoLayer), Layer.provide(emptyChannelMemberRepoLayer), Layer.provide(messageRepoLayer), ) - return buildServiceLayer(MessagePolicy).pipe( + return Layer.effect(MessagePolicy, MessagePolicy.make).pipe( Layer.provide(orgResolverLayer), Layer.provide(messageRepoLayer), Layer.provide(channelRepoLayer), @@ -117,7 +115,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither( - serviceEffect(MessagePolicy, (policy) => policy.canCreate(CHANNEL_ID)), + MessagePolicy.use((policy) => policy.canCreate(CHANNEL_ID)), layer, actor, ) @@ -126,7 +124,7 @@ describe("MessagePolicy", () => { it("canCreate denies non-org-member", async () => { const actor = makeActor({ - id: "00000000-0000-0000-0000-000000000199" as UserId, + id: "00000000-0000-4000-8000-000000000199" as UserId, }) const layer = makePolicyLayer( {}, @@ -137,7 +135,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither( - serviceEffect(MessagePolicy, (policy) => policy.canCreate(CHANNEL_ID)), + MessagePolicy.use((policy) => policy.canCreate(CHANNEL_ID)), layer, actor, ) @@ -157,7 +155,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither( - serviceEffect(MessagePolicy, (policy) => policy.canRead(CHANNEL_ID)), + MessagePolicy.use((policy) => policy.canRead(CHANNEL_ID)), layer, actor, ) @@ -179,7 +177,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither( - serviceEffect(MessagePolicy, (policy) => policy.canUpdate(MESSAGE_ID)), + MessagePolicy.use((policy) => policy.canUpdate(MESSAGE_ID)), layer, actor, ) @@ -188,7 +186,7 @@ describe("MessagePolicy", () => { it("canUpdate denies non-author", async () => { const otherUser = makeActor({ - id: "00000000-0000-0000-0000-000000000199" as UserId, + id: "00000000-0000-4000-8000-000000000199" as UserId, }) const layer = makePolicyLayer( { @@ -203,7 +201,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither( - serviceEffect(MessagePolicy, (policy) => policy.canUpdate(MESSAGE_ID)), + MessagePolicy.use((policy) => policy.canUpdate(MESSAGE_ID)), layer, otherUser, ) @@ -225,7 +223,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither( - serviceEffect(MessagePolicy, (policy) => policy.canDelete(MESSAGE_ID)), + MessagePolicy.use((policy) => policy.canDelete(MESSAGE_ID)), layer, actor, ) @@ -234,7 +232,7 @@ describe("MessagePolicy", () => { it("canDelete allows org admin who is not author", async () => { const admin = makeActor({ - id: "00000000-0000-0000-0000-000000000199" as UserId, + id: "00000000-0000-4000-8000-000000000199" as UserId, }) const layer = makePolicyLayer( { @@ -249,7 +247,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither( - serviceEffect(MessagePolicy, (policy) => policy.canDelete(MESSAGE_ID)), + MessagePolicy.use((policy) => policy.canDelete(MESSAGE_ID)), layer, admin, ) @@ -258,7 +256,7 @@ describe("MessagePolicy", () => { it("canDelete denies org member who is not author and not admin", async () => { const member = makeActor({ - id: "00000000-0000-0000-0000-000000000199" as UserId, + id: "00000000-0000-4000-8000-000000000199" as UserId, }) const layer = makePolicyLayer( { @@ -273,7 +271,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither( - serviceEffect(MessagePolicy, (policy) => policy.canDelete(MESSAGE_ID)), + MessagePolicy.use((policy) => policy.canDelete(MESSAGE_ID)), layer, member, ) @@ -293,7 +291,7 @@ describe("MessagePolicy", () => { ) const result = await runWithActorEither( - serviceEffect(MessagePolicy, (policy) => policy.canDelete(MISSING_MESSAGE_ID)), + MessagePolicy.use((policy) => policy.canDelete(MISSING_MESSAGE_ID)), layer, actor, ) diff --git a/apps/backend/src/policies/message-reaction-policy.test.ts b/apps/backend/src/policies/message-reaction-policy.test.ts index b8e150ca2..a397ca77a 100644 --- a/apps/backend/src/policies/message-reaction-policy.test.ts +++ b/apps/backend/src/policies/message-reaction-policy.test.ts @@ -20,21 +20,19 @@ import { MessageReactionPolicy } from "./message-reaction-policy.ts" import { ConnectConversationService } from "../services/connect-conversation-service.ts" import { OrgResolver } from "../services/org-resolver.ts" import { - buildServiceLayer, makeActor, makeEntityNotFound, runWithActorEither, - serviceEffect, serviceShape, TEST_ORG_ID, } from "./policy-test-helpers.ts" type Role = "admin" | "member" | "owner" -const CHANNEL_ID = "00000000-0000-0000-0000-000000000871" as ChannelId -const MESSAGE_ID = "00000000-0000-0000-0000-000000000872" as MessageId -const REACTION_ID = "00000000-0000-0000-0000-000000000873" as MessageReactionId -const OTHER_USER_ID = "00000000-0000-0000-0000-000000000874" as UserId +const CHANNEL_ID = "00000000-0000-4000-8000-000000000871" as ChannelId +const MESSAGE_ID = "00000000-0000-4000-8000-000000000872" as MessageId +const REACTION_ID = "00000000-0000-4000-8000-000000000873" as MessageReactionId +const OTHER_USER_ID = "00000000-0000-4000-8000-000000000874" as UserId type ReactionData = { userId: UserId } type MessageData = { channelId: ChannelId } @@ -104,14 +102,14 @@ const makePolicyLayer = ( const orgMemberRepoLayer = makeOrgMemberRepoLayer(orgMembers) // Build OrgResolver with actual channel data (not empty stubs) - const orgResolverLayer = buildServiceLayer(OrgResolver).pipe( + const orgResolverLayer = Layer.effect(OrgResolver, OrgResolver.make).pipe( Layer.provide(orgMemberRepoLayer), Layer.provide(channelRepoLayer), Layer.provide(emptyChannelMemberRepoLayer), Layer.provide(emptyMessageRepoLayer), ) - return buildServiceLayer(MessageReactionPolicy).pipe( + return Layer.effect(MessageReactionPolicy, MessageReactionPolicy.make).pipe( Layer.provide(makeReactionRepoLayer(reactions)), Layer.provide(messageRepoLayer), Layer.provide(orgResolverLayer), @@ -125,7 +123,7 @@ describe("MessageReactionPolicy", () => { const layer = makePolicyLayer({}, {}, {}, {}) const result = await runWithActorEither( - serviceEffect(MessageReactionPolicy, (policy) => policy.canList(MESSAGE_ID)), + MessageReactionPolicy.use((policy) => policy.canList(MESSAGE_ID)), layer, actor, ) @@ -142,7 +140,7 @@ describe("MessageReactionPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(MessageReactionPolicy, (policy) => policy.canCreate(MESSAGE_ID)), + MessageReactionPolicy.use((policy) => policy.canCreate(MESSAGE_ID)), layer, actor, ) @@ -159,7 +157,7 @@ describe("MessageReactionPolicy", () => { ) const result = await runWithActorEither( - serviceEffect(MessageReactionPolicy, (policy) => policy.canCreate(MESSAGE_ID)), + MessageReactionPolicy.use((policy) => policy.canCreate(MESSAGE_ID)), layer, outsider, ) @@ -171,7 +169,7 @@ describe("MessageReactionPolicy", () => { const layer = makePolicyLayer({}, { [REACTION_ID]: { userId: actor.id } }, {}, {}) const result = await runWithActorEither( - serviceEffect(MessageReactionPolicy, (policy) => policy.canUpdate(REACTION_ID)), + MessageReactionPolicy.use((policy) => policy.canUpdate(REACTION_ID)), layer, actor, ) @@ -183,7 +181,7 @@ describe("MessageReactionPolicy", () => { const layer = makePolicyLayer({}, { [REACTION_ID]: { userId: OTHER_USER_ID } }, {}, {}) const result = await runWithActorEither( - serviceEffect(MessageReactionPolicy, (policy) => policy.canUpdate(REACTION_ID)), + MessageReactionPolicy.use((policy) => policy.canUpdate(REACTION_ID)), layer, actor, ) @@ -195,7 +193,7 @@ describe("MessageReactionPolicy", () => { const layer = makePolicyLayer({}, { [REACTION_ID]: { userId: actor.id } }, {}, {}) const result = await runWithActorEither( - serviceEffect(MessageReactionPolicy, (policy) => policy.canDelete(REACTION_ID)), + MessageReactionPolicy.use((policy) => policy.canDelete(REACTION_ID)), layer, actor, ) @@ -207,7 +205,7 @@ describe("MessageReactionPolicy", () => { const layer = makePolicyLayer({}, { [REACTION_ID]: { userId: OTHER_USER_ID } }, {}, {}) const result = await runWithActorEither( - serviceEffect(MessageReactionPolicy, (policy) => policy.canDelete(REACTION_ID)), + MessageReactionPolicy.use((policy) => policy.canDelete(REACTION_ID)), layer, actor, ) diff --git a/apps/backend/src/policies/notification-policy.test.ts b/apps/backend/src/policies/notification-policy.test.ts index 859a7a057..5de8b83cb 100644 --- a/apps/backend/src/policies/notification-policy.test.ts +++ b/apps/backend/src/policies/notification-policy.test.ts @@ -9,21 +9,22 @@ import { makeEntityNotFound, makeOrgResolverLayer, runWithActorEither, + serviceShape, TEST_ORG_ID, } from "./policy-test-helpers.ts" type Role = "admin" | "member" | "owner" -const NOTIFICATION_ID = "00000000-0000-0000-0000-000000000841" as NotificationId -const MEMBER_ID = "00000000-0000-0000-0000-000000000842" as OrganizationMemberId -const ADMIN_USER_ID = "00000000-0000-0000-0000-000000000843" as UserId -const OTHER_USER_ID = "00000000-0000-0000-0000-000000000844" as UserId +const NOTIFICATION_ID = "00000000-0000-4000-8000-000000000841" as NotificationId +const MEMBER_ID = "00000000-0000-4000-8000-000000000842" as OrganizationMemberId +const ADMIN_USER_ID = "00000000-0000-4000-8000-000000000843" as UserId +const OTHER_USER_ID = "00000000-0000-4000-8000-000000000844" as UserId type NotificationData = { memberId: OrganizationMemberId } type MemberData = { userId: UserId; organizationId: OrganizationId; role: string } const makeNotificationRepoLayer = (notifications: Record) => - Layer.succeed(NotificationRepo, { + Layer.succeed(NotificationRepo, serviceShape({ with: ( id: NotificationId, f: (notification: NotificationData) => Effect.Effect, @@ -32,10 +33,10 @@ const makeNotificationRepoLayer = (notifications: Record, orgMembers: Record) => - Layer.succeed(OrganizationMemberRepo, { + Layer.succeed(OrganizationMemberRepo, serviceShape({ with: (id: OrganizationMemberId, f: (m: MemberData) => Effect.Effect) => { const member = members[id] if (!member) return Effect.fail(makeEntityNotFound("OrganizationMember")) @@ -49,14 +50,14 @@ const makeOrgMemberRepoLayer = (members: Record, orgMembers: const role = orgMembers[`${organizationId}:${userId}`] return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) }, - } as unknown as OrganizationMemberRepo) + })) const makePolicyLayer = ( notifications: Record, members: Record, orgMembers: Record, ) => - NotificationPolicy.DefaultWithoutDependencies.pipe( + Layer.effect(NotificationPolicy, NotificationPolicy.make).pipe( Layer.provide(makeNotificationRepoLayer(notifications)), Layer.provide(makeOrgMemberRepoLayer(members, orgMembers)), Layer.provide(makeOrgResolverLayer(orgMembers)), @@ -67,7 +68,11 @@ describe("NotificationPolicy", () => { const actor = makeActor() const layer = makePolicyLayer({}, {}, {}) - const result = await runWithActorEither(NotificationPolicy.canCreate(MEMBER_ID), layer, actor) + const result = await runWithActorEither( + NotificationPolicy.use((policy) => policy.canCreate(MEMBER_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -79,7 +84,11 @@ describe("NotificationPolicy", () => { {}, ) - const result = await runWithActorEither(NotificationPolicy.canView(NOTIFICATION_ID), layer, actor) + const result = await runWithActorEither( + NotificationPolicy.use((policy) => policy.canView(NOTIFICATION_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -91,7 +100,11 @@ describe("NotificationPolicy", () => { {}, ) - const result = await runWithActorEither(NotificationPolicy.canView(NOTIFICATION_ID), layer, actor) + const result = await runWithActorEither( + NotificationPolicy.use((policy) => policy.canView(NOTIFICATION_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -103,7 +116,11 @@ describe("NotificationPolicy", () => { { [`${TEST_ORG_ID}:${actor.id}`]: "member" }, ) - const result = await runWithActorEither(NotificationPolicy.canUpdate(NOTIFICATION_ID), layer, actor) + const result = await runWithActorEither( + NotificationPolicy.use((policy) => policy.canUpdate(NOTIFICATION_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -115,7 +132,11 @@ describe("NotificationPolicy", () => { { [`${TEST_ORG_ID}:${ADMIN_USER_ID}`]: "admin" }, ) - const result = await runWithActorEither(NotificationPolicy.canUpdate(NOTIFICATION_ID), layer, admin) + const result = await runWithActorEither( + NotificationPolicy.use((policy) => policy.canUpdate(NOTIFICATION_ID)), + layer, + admin, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -125,7 +146,7 @@ describe("NotificationPolicy", () => { { [NOTIFICATION_ID]: { memberId: MEMBER_ID } }, { [MEMBER_ID]: { - userId: "00000000-0000-0000-0000-000000000849" as UserId, + userId: "00000000-0000-4000-8000-000000000849" as UserId, organizationId: TEST_ORG_ID, role: "member", }, @@ -134,7 +155,7 @@ describe("NotificationPolicy", () => { ) const result = await runWithActorEither( - NotificationPolicy.canUpdate(NOTIFICATION_ID), + NotificationPolicy.use((policy) => policy.canUpdate(NOTIFICATION_ID)), layer, outsider, ) @@ -149,7 +170,11 @@ describe("NotificationPolicy", () => { { [`${TEST_ORG_ID}:${actor.id}`]: "member" }, ) - const result = await runWithActorEither(NotificationPolicy.canDelete(NOTIFICATION_ID), layer, actor) + const result = await runWithActorEither( + NotificationPolicy.use((policy) => policy.canDelete(NOTIFICATION_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -161,7 +186,11 @@ describe("NotificationPolicy", () => { { [`${TEST_ORG_ID}:${ADMIN_USER_ID}`]: "admin" }, ) - const result = await runWithActorEither(NotificationPolicy.canDelete(NOTIFICATION_ID), layer, admin) + const result = await runWithActorEither( + NotificationPolicy.use((policy) => policy.canDelete(NOTIFICATION_ID)), + layer, + admin, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -174,7 +203,7 @@ describe("NotificationPolicy", () => { ) const result = await runWithActorEither( - NotificationPolicy.canMarkAsRead(NOTIFICATION_ID), + NotificationPolicy.use((policy) => policy.canMarkAsRead(NOTIFICATION_ID)), layer, actor, ) @@ -189,7 +218,11 @@ describe("NotificationPolicy", () => { { [`${TEST_ORG_ID}:${actor.id}`]: "member" }, ) - const result = await runWithActorEither(NotificationPolicy.canMarkAllAsRead(MEMBER_ID), layer, actor) + const result = await runWithActorEither( + NotificationPolicy.use((policy) => policy.canMarkAllAsRead(MEMBER_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -201,7 +234,11 @@ describe("NotificationPolicy", () => { { [`${TEST_ORG_ID}:${ADMIN_USER_ID}`]: "admin" }, ) - const result = await runWithActorEither(NotificationPolicy.canMarkAllAsRead(MEMBER_ID), layer, admin) + const result = await runWithActorEither( + NotificationPolicy.use((policy) => policy.canMarkAllAsRead(MEMBER_ID)), + layer, + admin, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -211,7 +248,7 @@ describe("NotificationPolicy", () => { {}, { [MEMBER_ID]: { - userId: "00000000-0000-0000-0000-000000000849" as UserId, + userId: "00000000-0000-4000-8000-000000000849" as UserId, organizationId: TEST_ORG_ID, role: "member", }, @@ -220,7 +257,7 @@ describe("NotificationPolicy", () => { ) const result = await runWithActorEither( - NotificationPolicy.canMarkAllAsRead(MEMBER_ID), + NotificationPolicy.use((policy) => policy.canMarkAllAsRead(MEMBER_ID)), layer, outsider, ) diff --git a/apps/backend/src/policies/organization-member-policy.test.ts b/apps/backend/src/policies/organization-member-policy.test.ts index 1110d0329..a2bd1f117 100644 --- a/apps/backend/src/policies/organization-member-policy.test.ts +++ b/apps/backend/src/policies/organization-member-policy.test.ts @@ -9,20 +9,21 @@ import { makeEntityNotFound, makeOrgResolverLayer, runWithActorEither, + serviceShape, TEST_ORG_ID, } from "./policy-test-helpers.ts" type Role = "admin" | "member" | "owner" -const MEMBER_ID = "00000000-0000-0000-0000-000000000851" as OrganizationMemberId -const TARGET_USER_ID = "00000000-0000-0000-0000-000000000852" as UserId -const ADMIN_USER_ID = "00000000-0000-0000-0000-000000000853" as UserId -const OWNER_USER_ID = "00000000-0000-0000-0000-000000000854" as UserId +const MEMBER_ID = "00000000-0000-4000-8000-000000000851" as OrganizationMemberId +const TARGET_USER_ID = "00000000-0000-4000-8000-000000000852" as UserId +const ADMIN_USER_ID = "00000000-0000-4000-8000-000000000853" as UserId +const OWNER_USER_ID = "00000000-0000-4000-8000-000000000854" as UserId type MemberData = { userId: UserId; organizationId: OrganizationId; role: string } const makeOrgMemberRepoLayer = (membersById: Record, orgMembers: Record) => - Layer.succeed(OrganizationMemberRepo, { + Layer.succeed(OrganizationMemberRepo, serviceShape({ with: (id: OrganizationMemberId, f: (m: MemberData) => Effect.Effect) => { const member = membersById[id] if (!member) return Effect.fail(makeEntityNotFound("OrganizationMember")) @@ -32,10 +33,10 @@ const makeOrgMemberRepoLayer = (membersById: Record, orgMemb const role = orgMembers[`${organizationId}:${userId}`] return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) }, - } as unknown as OrganizationMemberRepo) + })) const makePolicyLayer = (membersById: Record, orgMembers: Record) => - OrganizationMemberPolicy.DefaultWithoutDependencies.pipe( + Layer.effect(OrganizationMemberPolicy, OrganizationMemberPolicy.make).pipe( Layer.provide(makeOrgMemberRepoLayer(membersById, orgMembers)), Layer.provide(makeOrgResolverLayer(orgMembers)), ) @@ -45,7 +46,11 @@ describe("OrganizationMemberPolicy", () => { const actor = makeActor() const layer = makePolicyLayer({}, {}) - const result = await runWithActorEither(OrganizationMemberPolicy.canCreate(TEST_ORG_ID), layer, actor) + const result = await runWithActorEither( + OrganizationMemberPolicy.use((policy) => policy.canCreate(TEST_ORG_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -53,7 +58,11 @@ describe("OrganizationMemberPolicy", () => { const actor = makeActor() const layer = makePolicyLayer({}, { [`${TEST_ORG_ID}:${actor.id}`]: "member" }) - const result = await runWithActorEither(OrganizationMemberPolicy.canCreate(TEST_ORG_ID), layer, actor) + const result = await runWithActorEither( + OrganizationMemberPolicy.use((policy) => policy.canCreate(TEST_ORG_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -64,7 +73,11 @@ describe("OrganizationMemberPolicy", () => { { [`${TEST_ORG_ID}:${actor.id}`]: "member" }, ) - const result = await runWithActorEither(OrganizationMemberPolicy.canUpdate(MEMBER_ID), layer, actor) + const result = await runWithActorEither( + OrganizationMemberPolicy.use((policy) => policy.canUpdate(MEMBER_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -75,7 +88,11 @@ describe("OrganizationMemberPolicy", () => { { [`${TEST_ORG_ID}:${ADMIN_USER_ID}`]: "admin" }, ) - const result = await runWithActorEither(OrganizationMemberPolicy.canUpdate(MEMBER_ID), layer, admin) + const result = await runWithActorEither( + OrganizationMemberPolicy.use((policy) => policy.canUpdate(MEMBER_ID)), + layer, + admin, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -86,7 +103,11 @@ describe("OrganizationMemberPolicy", () => { { [`${TEST_ORG_ID}:${OWNER_USER_ID}`]: "owner" }, ) - const result = await runWithActorEither(OrganizationMemberPolicy.canUpdate(MEMBER_ID), layer, owner) + const result = await runWithActorEither( + OrganizationMemberPolicy.use((policy) => policy.canUpdate(MEMBER_ID)), + layer, + owner, + ) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.failure)).toBe(true) @@ -94,14 +115,14 @@ describe("OrganizationMemberPolicy", () => { }) it("canUpdate denies outsider", async () => { - const outsider = makeActor({ id: "00000000-0000-0000-0000-000000000859" as UserId }) + const outsider = makeActor({ id: "00000000-0000-4000-8000-000000000859" as UserId }) const layer = makePolicyLayer( { [MEMBER_ID]: { userId: TARGET_USER_ID, organizationId: TEST_ORG_ID, role: "member" } }, {}, ) const result = await runWithActorEither( - OrganizationMemberPolicy.canUpdate(MEMBER_ID), + OrganizationMemberPolicy.use((policy) => policy.canUpdate(MEMBER_ID)), layer, outsider, ) @@ -115,7 +136,11 @@ describe("OrganizationMemberPolicy", () => { { [`${TEST_ORG_ID}:${actor.id}`]: "member" }, ) - const result = await runWithActorEither(OrganizationMemberPolicy.canDelete(MEMBER_ID), layer, actor) + const result = await runWithActorEither( + OrganizationMemberPolicy.use((policy) => policy.canDelete(MEMBER_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -126,7 +151,11 @@ describe("OrganizationMemberPolicy", () => { { [`${TEST_ORG_ID}:${ADMIN_USER_ID}`]: "admin" }, ) - const result = await runWithActorEither(OrganizationMemberPolicy.canDelete(MEMBER_ID), layer, admin) + const result = await runWithActorEither( + OrganizationMemberPolicy.use((policy) => policy.canDelete(MEMBER_ID)), + layer, + admin, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -137,7 +166,11 @@ describe("OrganizationMemberPolicy", () => { { [`${TEST_ORG_ID}:${OWNER_USER_ID}`]: "owner" }, ) - const result = await runWithActorEither(OrganizationMemberPolicy.canDelete(MEMBER_ID), layer, owner) + const result = await runWithActorEither( + OrganizationMemberPolicy.use((policy) => policy.canDelete(MEMBER_ID)), + layer, + owner, + ) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.failure)).toBe(true) diff --git a/apps/backend/src/policies/organization-policy.test.ts b/apps/backend/src/policies/organization-policy.test.ts index f65864480..433f51645 100644 --- a/apps/backend/src/policies/organization-policy.test.ts +++ b/apps/backend/src/policies/organization-policy.test.ts @@ -15,21 +15,24 @@ import { type Role = "admin" | "member" | "owner" const makePolicyLayer = (members: Record) => - OrganizationPolicy.DefaultWithoutDependencies.pipe( + Layer.effect(OrganizationPolicy, OrganizationPolicy.make).pipe( Layer.provide(makeOrgResolverLayer(members)), Layer.provide(makeOrganizationMemberRepoLayer(members)), ) describe("OrganizationPolicy", () => { it("canCreate allows any authenticated actor", async () => { - const result = await runWithActorEither(OrganizationPolicy.canCreate(), makePolicyLayer({})) + const result = await runWithActorEither( + OrganizationPolicy.use((policy) => policy.canCreate()), + makePolicyLayer({}), + ) expect(Result.isSuccess(result)).toBe(true) }) it("canUpdate allows admin and owner, denies plain member", async () => { const adminActor = makeActor() const memberActor = makeActor({ - id: "00000000-0000-0000-0000-000000000222" as UserId, + id: "00000000-0000-4000-8000-000000000222" as UserId, }) const layer = makePolicyLayer({ @@ -38,12 +41,12 @@ describe("OrganizationPolicy", () => { }) const adminResult = await runWithActorEither( - OrganizationPolicy.canUpdate(TEST_ORG_ID), + OrganizationPolicy.use((policy) => policy.canUpdate(TEST_ORG_ID)), layer, adminActor, ) const memberResult = await runWithActorEither( - OrganizationPolicy.canUpdate(TEST_ORG_ID), + OrganizationPolicy.use((policy) => policy.canUpdate(TEST_ORG_ID)), layer, memberActor, ) @@ -58,7 +61,7 @@ describe("OrganizationPolicy", () => { it("canDelete allows owner only", async () => { const ownerActor = makeActor() const adminActor = makeActor({ - id: "00000000-0000-0000-0000-000000000223" as UserId, + id: "00000000-0000-4000-8000-000000000223" as UserId, }) const layer = makePolicyLayer({ @@ -67,12 +70,12 @@ describe("OrganizationPolicy", () => { }) const ownerResult = await runWithActorEither( - OrganizationPolicy.canDelete(TEST_ORG_ID), + OrganizationPolicy.use((policy) => policy.canDelete(TEST_ORG_ID)), layer, ownerActor, ) const adminResult = await runWithActorEither( - OrganizationPolicy.canDelete(TEST_ORG_ID), + OrganizationPolicy.use((policy) => policy.canDelete(TEST_ORG_ID)), layer, adminActor, ) @@ -87,7 +90,11 @@ describe("OrganizationPolicy", () => { [`${TEST_ALT_ORG_ID}:${actor.id}`]: "member", }) - const result = await runWithActorEither(OrganizationPolicy.isMember(TEST_ORG_ID), layer, actor) + const result = await runWithActorEither( + OrganizationPolicy.use((policy) => policy.isMember(TEST_ORG_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.failure)).toBe(true) @@ -101,7 +108,7 @@ describe("OrganizationPolicy", () => { }) const result = await runWithActorEither( - OrganizationPolicy.canManagePublicInvite(TEST_ORG_ID), + OrganizationPolicy.use((policy) => policy.canManagePublicInvite(TEST_ORG_ID)), layer, actor, ) diff --git a/apps/backend/src/policies/pinned-message-policy.test.ts b/apps/backend/src/policies/pinned-message-policy.test.ts index 77d5fa067..edebc0a65 100644 --- a/apps/backend/src/policies/pinned-message-policy.test.ts +++ b/apps/backend/src/policies/pinned-message-policy.test.ts @@ -9,51 +9,52 @@ import { makeEntityNotFound, makeOrgResolverLayer, runWithActorEither, + serviceShape, TEST_ORG_ID, } from "./policy-test-helpers.ts" type Role = "admin" | "member" | "owner" -const CHANNEL_ID = "00000000-0000-0000-0000-000000000861" as ChannelId -const PINNED_MSG_ID = "00000000-0000-0000-0000-000000000862" as PinnedMessageId -const ADMIN_USER_ID = "00000000-0000-0000-0000-000000000863" as UserId -const OTHER_USER_ID = "00000000-0000-0000-0000-000000000864" as UserId +const CHANNEL_ID = "00000000-0000-4000-8000-000000000861" as ChannelId +const PINNED_MSG_ID = "00000000-0000-4000-8000-000000000862" as PinnedMessageId +const ADMIN_USER_ID = "00000000-0000-4000-8000-000000000863" as UserId +const OTHER_USER_ID = "00000000-0000-4000-8000-000000000864" as UserId type ChannelData = { organizationId: OrganizationId; type: string; id: string } type PinnedData = { pinnedBy: UserId; channelId: ChannelId } const makePinnedMessageRepoLayer = (pinnedMessages: Record) => - Layer.succeed(PinnedMessageRepo, { + Layer.succeed(PinnedMessageRepo, serviceShape({ with: (id: PinnedMessageId, f: (pm: PinnedData) => Effect.Effect) => { const pm = pinnedMessages[id] if (!pm) return Effect.fail(makeEntityNotFound("PinnedMessage")) return f(pm) }, - } as unknown as PinnedMessageRepo) + })) const makeChannelRepoLayer = (channels: Record) => - Layer.succeed(ChannelRepo, { + Layer.succeed(ChannelRepo, serviceShape({ with: (id: ChannelId, f: (ch: ChannelData) => Effect.Effect) => { const ch = channels[id] if (!ch) return Effect.fail(makeEntityNotFound("Channel")) return f(ch) }, - } as unknown as ChannelRepo) + })) const makeOrgMemberRepoLayer = (orgMembers: Record) => - Layer.succeed(OrganizationMemberRepo, { + Layer.succeed(OrganizationMemberRepo, serviceShape({ findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { const role = orgMembers[`${organizationId}:${userId}`] return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) }, - } as unknown as OrganizationMemberRepo) + })) const makePolicyLayer = ( orgMembers: Record, channels: Record, pinnedMessages: Record, ) => - PinnedMessagePolicy.DefaultWithoutDependencies.pipe( + Layer.effect(PinnedMessagePolicy, PinnedMessagePolicy.make).pipe( Layer.provide(makePinnedMessageRepoLayer(pinnedMessages)), Layer.provide(makeChannelRepoLayer(channels)), Layer.provide(makeOrgMemberRepoLayer(orgMembers)), @@ -69,7 +70,11 @@ describe("PinnedMessagePolicy", () => { {}, ) - const result = await runWithActorEither(PinnedMessagePolicy.canCreate(CHANNEL_ID), layer, admin) + const result = await runWithActorEither( + PinnedMessagePolicy.use((policy) => policy.canCreate(CHANNEL_ID)), + layer, + admin, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -81,7 +86,11 @@ describe("PinnedMessagePolicy", () => { {}, ) - const result = await runWithActorEither(PinnedMessagePolicy.canCreate(CHANNEL_ID), layer, actor) + const result = await runWithActorEither( + PinnedMessagePolicy.use((policy) => policy.canCreate(CHANNEL_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -93,7 +102,11 @@ describe("PinnedMessagePolicy", () => { {}, ) - const result = await runWithActorEither(PinnedMessagePolicy.canCreate(CHANNEL_ID), layer, actor) + const result = await runWithActorEither( + PinnedMessagePolicy.use((policy) => policy.canCreate(CHANNEL_ID)), + layer, + actor, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -105,7 +118,11 @@ describe("PinnedMessagePolicy", () => { {}, ) - const result = await runWithActorEither(PinnedMessagePolicy.canCreate(CHANNEL_ID), layer, outsider) + const result = await runWithActorEither( + PinnedMessagePolicy.use((policy) => policy.canCreate(CHANNEL_ID)), + layer, + outsider, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -117,7 +134,11 @@ describe("PinnedMessagePolicy", () => { { [PINNED_MSG_ID]: { pinnedBy: actor.id, channelId: CHANNEL_ID } }, ) - const result = await runWithActorEither(PinnedMessagePolicy.canUpdate(PINNED_MSG_ID), layer, actor) + const result = await runWithActorEither( + PinnedMessagePolicy.use((policy) => policy.canUpdate(PINNED_MSG_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -129,7 +150,11 @@ describe("PinnedMessagePolicy", () => { { [PINNED_MSG_ID]: { pinnedBy: OTHER_USER_ID, channelId: CHANNEL_ID } }, ) - const result = await runWithActorEither(PinnedMessagePolicy.canUpdate(PINNED_MSG_ID), layer, admin) + const result = await runWithActorEither( + PinnedMessagePolicy.use((policy) => policy.canUpdate(PINNED_MSG_ID)), + layer, + admin, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -141,7 +166,11 @@ describe("PinnedMessagePolicy", () => { { [PINNED_MSG_ID]: { pinnedBy: ADMIN_USER_ID, channelId: CHANNEL_ID } }, ) - const result = await runWithActorEither(PinnedMessagePolicy.canUpdate(PINNED_MSG_ID), layer, outsider) + const result = await runWithActorEither( + PinnedMessagePolicy.use((policy) => policy.canUpdate(PINNED_MSG_ID)), + layer, + outsider, + ) expect(Result.isFailure(result)).toBe(true) }) @@ -153,7 +182,11 @@ describe("PinnedMessagePolicy", () => { { [PINNED_MSG_ID]: { pinnedBy: actor.id, channelId: CHANNEL_ID } }, ) - const result = await runWithActorEither(PinnedMessagePolicy.canDelete(PINNED_MSG_ID), layer, actor) + const result = await runWithActorEither( + PinnedMessagePolicy.use((policy) => policy.canDelete(PINNED_MSG_ID)), + layer, + actor, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -165,7 +198,11 @@ describe("PinnedMessagePolicy", () => { { [PINNED_MSG_ID]: { pinnedBy: OTHER_USER_ID, channelId: CHANNEL_ID } }, ) - const result = await runWithActorEither(PinnedMessagePolicy.canDelete(PINNED_MSG_ID), layer, admin) + const result = await runWithActorEither( + PinnedMessagePolicy.use((policy) => policy.canDelete(PINNED_MSG_ID)), + layer, + admin, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -177,7 +214,11 @@ describe("PinnedMessagePolicy", () => { { [PINNED_MSG_ID]: { pinnedBy: ADMIN_USER_ID, channelId: CHANNEL_ID } }, ) - const result = await runWithActorEither(PinnedMessagePolicy.canDelete(PINNED_MSG_ID), layer, outsider) + const result = await runWithActorEither( + PinnedMessagePolicy.use((policy) => policy.canDelete(PINNED_MSG_ID)), + layer, + outsider, + ) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { expect(UnauthorizedError.is(result.failure)).toBe(true) diff --git a/apps/backend/src/policies/policy-test-helpers.ts b/apps/backend/src/policies/policy-test-helpers.ts index b0ce90f7a..f41054d52 100644 --- a/apps/backend/src/policies/policy-test-helpers.ts +++ b/apps/backend/src/policies/policy-test-helpers.ts @@ -5,7 +5,8 @@ import { CurrentRpcScopes } from "@hazel/domain/scopes" import type { ChannelId, ChannelMemberId, MessageId, OrganizationId, UserId } from "@hazel/schema" import { Effect, Layer, Option } from "effect" import { OrgResolver } from "../services/org-resolver" -export { buildServiceLayer, serviceEffect, serviceShape } from "../test/effect-helpers" +import { serviceShape } from "../test/effect-helpers" +export { serviceShape } from "../test/effect-helpers" export const TEST_ORG_ID = "00000000-0000-0000-0000-000000000001" as OrganizationId export const TEST_ALT_ORG_ID = "00000000-0000-0000-0000-000000000002" as OrganizationId @@ -37,7 +38,7 @@ export const runWithActorEither = ( Effect.provide(layer), Effect.provideService(CurrentUser.Context, actor), Effect.result, - ), + ) as Effect.Effect, ) export const makeEntityNotFound = (entity = "Entity") => @@ -52,41 +53,44 @@ type Role = "admin" | "member" | "owner" * Creates a mock OrganizationMemberRepo layer for testing. */ export const makeOrganizationMemberRepoLayer = (members: Record) => - Layer.succeed(OrganizationMemberRepo, { + Layer.succeed(OrganizationMemberRepo, serviceShape({ findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { const role = members[`${organizationId}:${userId}`] return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) }, - } as unknown as OrganizationMemberRepo) + })) /** * Creates a stub repo layer that returns none/empty for all lookups. * Used for OrgResolver dependencies that aren't relevant to a specific test. */ -const emptyChannelRepoLayer = Layer.succeed(ChannelRepo, { +const emptyChannelRepoLayer = Layer.succeed(ChannelRepo, serviceShape({ findById: (_id: ChannelId) => Effect.succeed(Option.none()), with: (_id: ChannelId, _f: (c: any) => Effect.Effect) => Effect.fail(makeEntityNotFound("Channel")), -} as unknown as ChannelRepo) +})) -const emptyChannelMemberRepoLayer = Layer.succeed(ChannelMemberRepo, { +const emptyChannelMemberRepoLayer = Layer.succeed( + ChannelMemberRepo, + serviceShape({ findByChannelAndUser: (_channelId: ChannelId, _userId: UserId) => Effect.succeed(Option.none()), with: (_id: ChannelMemberId, _f: (c: any) => Effect.Effect) => Effect.fail(makeEntityNotFound("ChannelMember")), -} as unknown as ChannelMemberRepo) + }), +) -const emptyMessageRepoLayer = Layer.succeed(MessageRepo, { +const emptyMessageRepoLayer = Layer.succeed(MessageRepo, serviceShape({ findById: (_id: MessageId) => Effect.succeed(Option.none()), with: (_id: MessageId, _f: (c: any) => Effect.Effect) => Effect.fail(makeEntityNotFound("Message")), -} as unknown as MessageRepo) +})) /** * Creates an OrgResolver layer backed by the given member mock. * Provides stub repos for channels, channel members, and messages. */ export const makeOrgResolverLayer = (members: Record) => - buildServiceLayer(OrgResolver).pipe( + Layer.effect(OrgResolver, OrgResolver.make).pipe( Layer.provide(makeOrganizationMemberRepoLayer(members)), Layer.provide(emptyChannelRepoLayer), Layer.provide(emptyChannelMemberRepoLayer), diff --git a/apps/backend/src/policies/typing-indicator-policy.test.ts b/apps/backend/src/policies/typing-indicator-policy.test.ts index e14ffc835..b0241b963 100644 --- a/apps/backend/src/policies/typing-indicator-policy.test.ts +++ b/apps/backend/src/policies/typing-indicator-policy.test.ts @@ -4,13 +4,19 @@ import { UnauthorizedError } from "@hazel/domain" import type { ChannelId, ChannelMemberId, TypingIndicatorId, UserId } from "@hazel/schema" import { Effect, Result, Layer, Option } from "effect" import { TypingIndicatorPolicy } from "./typing-indicator-policy.ts" -import { makeActor, makeEntityNotFound, runWithActorEither, TEST_ORG_ID } from "./policy-test-helpers.ts" - -const CHANNEL_ID = "00000000-0000-0000-0000-000000000501" as ChannelId -const MEMBER_ID = "00000000-0000-0000-0000-000000000601" as ChannelMemberId -const OTHER_MEMBER_ID = "00000000-0000-0000-0000-000000000602" as ChannelMemberId -const INDICATOR_ID = "00000000-0000-0000-0000-000000000701" as TypingIndicatorId -const MISSING_INDICATOR_ID = "00000000-0000-0000-0000-000000000799" as TypingIndicatorId +import { + makeActor, + makeEntityNotFound, + runWithActorEither, + serviceShape, + TEST_ORG_ID, +} from "./policy-test-helpers.ts" + +const CHANNEL_ID = "00000000-0000-4000-8000-000000000501" as ChannelId +const MEMBER_ID = "00000000-0000-4000-8000-000000000601" as ChannelMemberId +const OTHER_MEMBER_ID = "00000000-0000-4000-8000-000000000602" as ChannelMemberId +const INDICATOR_ID = "00000000-0000-4000-8000-000000000701" as TypingIndicatorId +const MISSING_INDICATOR_ID = "00000000-0000-4000-8000-000000000799" as TypingIndicatorId type MemberRecord = { id: ChannelMemberId @@ -24,7 +30,7 @@ const makeChannelMemberRepoLayer = ( recordsByMemberId: Record, recordsByChannelAndUser: Record, ) => - Layer.succeed(ChannelMemberRepo, { + Layer.succeed(ChannelMemberRepo, serviceShape({ findByChannelAndUser: (channelId: ChannelId, userId: UserId) => Effect.succeed(Option.fromNullishOr(recordsByChannelAndUser[`${channelId}:${userId}`])), with: (id: ChannelMemberId, f: (member: MemberRecord) => Effect.Effect) => { @@ -34,10 +40,10 @@ const makeChannelMemberRepoLayer = ( } return f(member) }, - } as unknown as ChannelMemberRepo) + })) const makeTypingIndicatorRepoLayer = (recordsById: Record) => - Layer.succeed(TypingIndicatorRepo, { + Layer.succeed(TypingIndicatorRepo, serviceShape({ with: (id: TypingIndicatorId, f: (indicator: IndicatorRecord) => Effect.Effect) => { const indicator = recordsById[id] if (!indicator) { @@ -45,14 +51,14 @@ const makeTypingIndicatorRepoLayer = (recordsById: Record, channelMembersByChannelAndUser: Record, indicatorsById: Record, ) => - TypingIndicatorPolicy.DefaultWithoutDependencies.pipe( + Layer.effect(TypingIndicatorPolicy, TypingIndicatorPolicy.make).pipe( Layer.provide(makeChannelMemberRepoLayer(channelMembersById, channelMembersByChannelAndUser)), Layer.provide(makeTypingIndicatorRepoLayer(indicatorsById)), ) @@ -60,7 +66,10 @@ const makePolicyLayer = ( describe("TypingIndicatorPolicy", () => { it("canRead always allows authenticated actors", async () => { const layer = makePolicyLayer({}, {}, {}) - const result = await runWithActorEither(TypingIndicatorPolicy.canRead(INDICATOR_ID), layer) + const result = await runWithActorEither( + TypingIndicatorPolicy.use((policy) => policy.canRead(INDICATOR_ID)), + layer, + ) expect(Result.isSuccess(result)).toBe(true) }) @@ -79,9 +88,15 @@ describe("TypingIndicatorPolicy", () => { {}, ) - const allowed = await runWithActorEither(TypingIndicatorPolicy.canCreate(CHANNEL_ID), layer, actor) + const allowed = await runWithActorEither( + TypingIndicatorPolicy.use((policy) => policy.canCreate(CHANNEL_ID)), + layer, + actor, + ) const denied = await runWithActorEither( - TypingIndicatorPolicy.canCreate("00000000-0000-0000-0000-000000000599" as ChannelId), + TypingIndicatorPolicy.use((policy) => + policy.canCreate("00000000-0000-4000-8000-000000000599" as ChannelId), + ), layer, actor, ) @@ -93,7 +108,7 @@ describe("TypingIndicatorPolicy", () => { it("canUpdate allows only the member owner and maps missing indicator to UnauthorizedError", async () => { const actor = makeActor() const otherActor = makeActor({ - id: "00000000-0000-0000-0000-000000000103" as UserId, + id: "00000000-0000-4000-8000-000000000103" as UserId, }) const layer = makePolicyLayer( @@ -116,17 +131,17 @@ describe("TypingIndicatorPolicy", () => { ) const ownerAllowed = await runWithActorEither( - TypingIndicatorPolicy.canUpdate(INDICATOR_ID), + TypingIndicatorPolicy.use((policy) => policy.canUpdate(INDICATOR_ID)), layer, actor, ) const otherDenied = await runWithActorEither( - TypingIndicatorPolicy.canUpdate(INDICATOR_ID), + TypingIndicatorPolicy.use((policy) => policy.canUpdate(INDICATOR_ID)), layer, otherActor, ) const missingDenied = await runWithActorEither( - TypingIndicatorPolicy.canUpdate(MISSING_INDICATOR_ID), + TypingIndicatorPolicy.use((policy) => policy.canUpdate(MISSING_INDICATOR_ID)), layer, actor, ) @@ -152,7 +167,7 @@ describe("TypingIndicatorPolicy", () => { [OTHER_MEMBER_ID]: { id: OTHER_MEMBER_ID, channelId: CHANNEL_ID, - userId: "00000000-0000-0000-0000-000000000109" as UserId, + userId: "00000000-0000-4000-8000-000000000109" as UserId, organizationId: TEST_ORG_ID, }, }, @@ -167,17 +182,17 @@ describe("TypingIndicatorPolicy", () => { ) const byMemberAllowed = await runWithActorEither( - TypingIndicatorPolicy.canDelete({ memberId: MEMBER_ID }), + TypingIndicatorPolicy.use((policy) => policy.canDelete({ memberId: MEMBER_ID })), layer, actor, ) const byIndicatorAllowed = await runWithActorEither( - TypingIndicatorPolicy.canDelete({ id: INDICATOR_ID }), + TypingIndicatorPolicy.use((policy) => policy.canDelete({ id: INDICATOR_ID })), layer, actor, ) const byMemberDenied = await runWithActorEither( - TypingIndicatorPolicy.canDelete({ memberId: OTHER_MEMBER_ID }), + TypingIndicatorPolicy.use((policy) => policy.canDelete({ memberId: OTHER_MEMBER_ID })), layer, actor, ) diff --git a/apps/backend/src/routes/auth.http.test.ts b/apps/backend/src/routes/auth.http.test.ts index dd3f77b56..8a62b3615 100644 --- a/apps/backend/src/routes/auth.http.test.ts +++ b/apps/backend/src/routes/auth.http.test.ts @@ -1,9 +1,11 @@ +import { WorkOS as WorkOSNodeAPI } from "@workos-inc/node" import { describe, expect, it, layer } from "@effect/vitest" import { OrganizationMemberRepo, UserRepo } from "@hazel/backend-core" +import { OrganizationMember, User } from "@hazel/domain/models" import type { OrganizationId, UserId } from "@hazel/schema" import { Effect, Layer, Option, Schema, ServiceMap } from "effect" import { AuthState, RelativeUrl } from "../lib/schema.ts" -import { configLayer } from "../test/effect-helpers" +import { configLayer, serviceShape } from "../test/effect-helpers" import { WorkOSAuth as WorkOS, WorkOSAuthError as WorkOSApiError } from "../services/workos-auth.ts" // ===== Mock Configuration ===== @@ -15,6 +17,26 @@ const TestConfigLive = configLayer({ WORKOS_API_KEY: "sk_test_123", }) +const NOW = new Date("2026-03-05T12:00:00.000Z") + +const makeUserRecord = (overrides: Partial> = {}) => + ({ + id: "usr_default123" as UserId, + externalId: "user_default", + email: "test@example.com", + firstName: "Test", + lastName: "User", + avatarUrl: null, + userType: "user", + settings: null, + isOnboarded: false, + timezone: null, + createdAt: NOW, + updatedAt: NOW, + deletedAt: null, + ...overrides, + }) satisfies Schema.Schema.Type + // ===== Mock WorkOS Service ===== const createMockWorkOSLive = (options?: { @@ -34,69 +56,73 @@ const createMockWorkOSLive = (options?: { shouldFailLogin?: boolean shouldFailGetOrg?: boolean }) => - Layer.succeed(WorkOS, { - call:
(f: (client: any, signal: AbortSignal) => Promise) => - Effect.tryPromise({ - try: async () => { - const mockClient = { - userManagement: { - getAuthorizationUrl: (params: any) => { - if (options?.shouldFailLogin) { - throw new Error("WorkOS API error") - } - return ( - options?.authorizationUrl ?? - `https://workos.com/auth?client_id=${params.clientId}&state=${params.state}` - ) - }, - authenticateWithCode: async () => { - if (options?.shouldFailAuth) { - throw new Error("Authentication failed") - } - return { - user: options?.authenticateResponse?.user ?? { - id: "user_01ABC123", - email: "test@example.com", - firstName: "Test", - lastName: "User", - profilePictureUrl: null, - }, - sealedSession: - options?.authenticateResponse?.sealedSession ?? - "sealed-session-cookie", - organizationId: options?.authenticateResponse?.organizationId, - } - }, - listOrganizationMemberships: async () => ({ - data: [{ role: { slug: "member" } }], - }), - }, - organizations: { - getOrganization: async (id: string) => { - if (options?.shouldFailGetOrg) { - throw new Error("Org not found") - } - return { - id, - externalId: "org_internal_123", - } + Layer.succeed( + WorkOS, + ({ + call: (f: (client: WorkOSNodeAPI, signal: AbortSignal) => Promise) => + Effect.tryPromise({ + try: async () => { + const mockClient = { + userManagement: { + getAuthorizationUrl: (params: { clientId: string; state?: string }) => { + if (options?.shouldFailLogin) { + throw new Error("WorkOS API error") + } + return ( + options?.authorizationUrl ?? + `https://workos.com/auth?client_id=${params.clientId}&state=${params.state}` + ) + }, + authenticateWithCode: async () => { + if (options?.shouldFailAuth) { + throw new Error("Authentication failed") + } + return { + user: options?.authenticateResponse?.user ?? { + id: "user_01ABC123", + email: "test@example.com", + firstName: "Test", + lastName: "User", + profilePictureUrl: null, + }, + sealedSession: + options?.authenticateResponse?.sealedSession ?? + "sealed-session-cookie", + organizationId: options?.authenticateResponse?.organizationId, + } + }, + listOrganizationMemberships: async () => ({ + data: [{ role: { slug: "member" } }], + }), }, - getOrganizationByExternalId: async (externalId: string) => { - if (options?.shouldFailGetOrg) { - throw new Error("Org not found") - } - return { - id: "org_workos_123", - externalId, - } + organizations: { + getOrganization: async (id: string) => { + if (options?.shouldFailGetOrg) { + throw new Error("Org not found") + } + return { + id, + externalId: "org_internal_123", + } + }, + getOrganizationByExternalId: async (externalId: string) => { + if (options?.shouldFailGetOrg) { + throw new Error("Org not found") + } + return { + id: "org_workos_123", + externalId, + } + }, }, - }, - } - return f(mockClient as any, new AbortController().signal) - }, - catch: (cause) => new WorkOSApiError({ cause }), - }), - } as ServiceMap.Service.Shape) + } + + return f(mockClient as unknown as WorkOSNodeAPI, new AbortController().signal) + }, + catch: (cause) => new WorkOSApiError({ cause }), + }), + }) satisfies ServiceMap.Service.Shape, + ) // ===== Mock UserRepo ===== @@ -114,24 +140,30 @@ const createMockUserRepoLive = (options?: { Layer.succeed(UserRepo, { findByExternalId: (_externalId: string) => Effect.succeed(options?.existingUser ? Option.some(options.existingUser) : Option.none()), - upsertByExternalId: (user: any) => - Effect.succeed({ - id: "usr_new123" as UserId, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - avatarUrl: user.avatarUrl, - isOnboarded: user.isOnboarded, - timezone: user.timezone, - }), - } as unknown as UserRepo) + upsertByExternalId: (user: Schema.Schema.Type) => + Effect.succeed( + makeUserRecord({ + id: "usr_new123" as UserId, + externalId: user.externalId, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + avatarUrl: user.avatarUrl ?? null, + isOnboarded: user.isOnboarded, + timezone: user.timezone, + }), + ), + } as unknown as ServiceMap.Service.Shape) // ===== Mock OrganizationMemberRepo ===== -const MockOrganizationMemberRepoLive = Layer.succeed(OrganizationMemberRepo, { +const MockOrganizationMemberRepoLive = Layer.succeed(OrganizationMemberRepo, serviceShape({ findByOrgAndUser: (_orgId: OrganizationId, _userId: UserId) => Effect.succeed(Option.none()), - upsertByOrgAndUser: (_membership: any) => Effect.succeed({}), -} as unknown as OrganizationMemberRepo) + upsertByOrgAndUser: (_membership: Schema.Schema.Type) => + Effect.succeed({ + id: "00000000-0000-4000-8000-000000000099", + }), +})) // ===== Test Layer Factory ===== @@ -176,19 +208,19 @@ describe("Auth HTTP Endpoint Logic", () => { }) }) - describe("AuthState schema", () => { - it("creates valid AuthState", () => { - const state = AuthState.make({ returnTo: "/dashboard" }) - expect(state.returnTo).toBe("/dashboard") - }) + describe("AuthState schema", () => { + it("creates valid AuthState", () => { + const state = Schema.decodeSync(AuthState)({ returnTo: "/dashboard" }) + expect(state.returnTo).toBe("/dashboard") + }) - it("serializes and deserializes correctly", () => { - const state = AuthState.make({ returnTo: "/settings/profile" }) - const serialized = JSON.stringify(state) - const parsed = AuthState.make(JSON.parse(serialized)) - expect(parsed.returnTo).toBe("/settings/profile") + it("serializes and deserializes correctly", () => { + const state = Schema.decodeSync(AuthState)({ returnTo: "/settings/profile" }) + const serialized = JSON.stringify(state) + const parsed = Schema.decodeSync(AuthState)(JSON.parse(serialized)) + expect(parsed.returnTo).toBe("/settings/profile") + }) }) - }) describe("Login flow", () => { layer(TestLayer)("authorization URL generation", (it) => { @@ -300,9 +332,7 @@ describe("Auth HTTP Endpoint Logic", () => { Effect.gen(function* () { const userRepo = yield* UserRepo - const existingUser = yield* userRepo.findByExternalId( - "user_new", - ) as Effect.Effect + const existingUser = yield* userRepo.findByExternalId("user_new") expect(Option.isNone(existingUser)).toBe(true) const createdUser = yield* userRepo.upsertByExternalId({ @@ -316,7 +346,7 @@ describe("Auth HTTP Endpoint Logic", () => { isOnboarded: false, timezone: null, deletedAt: null, - }) as Effect.Effect + }) expect(createdUser.id).toBe("usr_new123") expect(createdUser.email).toBe("new@example.com") @@ -347,7 +377,7 @@ describe("Auth HTTP Endpoint Logic", () => { id: UserId email: string isOnboarded: boolean - }> = yield* userRepo.findByExternalId("user_existing") as Effect.Effect + }> = yield* userRepo.findByExternalId("user_existing") expect(Option.isSome(existingUser)).toBe(true) if (Option.isSome(existingUser)) { diff --git a/apps/backend/src/routes/bot-commands.http.test.ts b/apps/backend/src/routes/bot-commands.http.test.ts index 250992f0a..4f8b72c18 100644 --- a/apps/backend/src/routes/bot-commands.http.test.ts +++ b/apps/backend/src/routes/bot-commands.http.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "@effect/vitest" import { Effect, Fiber, Stream } from "effect" -import { createCommandSseStream, createSseHeartbeatStream } from "./bot-commands.http.ts" +import { + createCommandSseStream, + createSseHeartbeatStream, + type CommandSseRedis, +} from "./bot-commands.sse.ts" describe("bot command SSE streams", () => { it("emits an immediate heartbeat on connect", () => @@ -20,10 +24,10 @@ describe("bot command SSE streams", () => { const fiber = yield* createSseHeartbeatStream("5 millis").pipe( Stream.take(3), Stream.runCollect, - Effect.fork, + Effect.forkDetach, ) - const events = yield* Fiber.join(fiber) + const events = Array.from(yield* Fiber.join(fiber)) as string[] expect(events).toHaveLength(3) for (const event of events) { @@ -43,7 +47,7 @@ describe("bot command SSE streams", () => { timestamp: Date.now(), }) - const redisMock = { + const redisMock: CommandSseRedis = { subscribe: (channel: string, handler: (message: string, chan: string) => void) => Effect.sync(() => { queueMicrotask(() => { @@ -57,14 +61,14 @@ describe("bot command SSE streams", () => { botId: "bot_test", botName: "Test Bot", channel: "bot:bot_test:commands", - redis: redisMock as any, + redis: redisMock, heartbeatInterval: "1 hour", }) - const collector = yield* stream.pipe(Stream.take(2), Stream.runCollect, Effect.fork) + const collector = yield* stream.pipe(Stream.take(2), Stream.runCollect, Effect.forkDetach) - const events = yield* Fiber.join(collector) - const commandEvent = Array.from(events).find((event) => event.includes("event: command")) + const events = Array.from(yield* Fiber.join(collector)) as string[] + const commandEvent = events.find((event) => event.includes("event: command")) expect(commandEvent).toBeDefined() expect(commandEvent).toContain(`data: ${payload}`) diff --git a/apps/backend/src/routes/bot-commands.http.ts b/apps/backend/src/routes/bot-commands.http.ts index 5d52d63ba..51589aeea 100644 --- a/apps/backend/src/routes/bot-commands.http.ts +++ b/apps/backend/src/routes/bot-commands.http.ts @@ -1,6 +1,5 @@ import { HttpApiBuilder } from "effect/unstable/httpapi" import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http" -import { Sse } from "effect/unstable/encoding" import { BotCommandRepo, BotInstallationRepo, BotRepo, IntegrationConnectionRepo } from "@hazel/backend-core" import { CurrentUser, InternalServerError, UnauthorizedError } from "@hazel/domain" import { @@ -18,10 +17,11 @@ import { UpdateBotSettingsResponse, } from "@hazel/domain/http" import { Redis } from "@hazel/effect-bun" -import { ServiceMap, Cause, Duration, Effect, Option, Queue, Schedule, Stream } from "effect" +import { Cause, Effect, Option, Stream } from "effect" import { HazelApi } from "../api.ts" import { BotGatewayService } from "../services/bot-gateway-service.ts" import { IntegrationTokenService } from "../services/integration-token-service.ts" +import { createCommandSseStream } from "./bot-commands.sse.ts" /** * Hash a token using SHA-256 (Web Crypto API) @@ -68,89 +68,6 @@ const validateBotToken = Effect.gen(function* () { return botOption.value }) -const HEARTBEAT_INTERVAL = "25 seconds" as const - -const encodeSseEvent = (event: string, data: string) => - Sse.encoder.write({ - _tag: "Event", - event, - id: undefined, - data, - }) - -export const createSseHeartbeatStream = (interval: Duration.Input = HEARTBEAT_INTERVAL) => - Stream.make( - encodeSseEvent( - "heartbeat", - JSON.stringify({ - type: "heartbeat", - timestamp: Date.now(), - }), - ), - ).pipe( - Stream.concat( - Stream.fromSchedule(Schedule.spaced(interval)).pipe( - Stream.map(() => - encodeSseEvent( - "heartbeat", - JSON.stringify({ - type: "heartbeat", - timestamp: Date.now(), - }), - ), - ), - ), - ), - ) - -interface CommandSseStreamOptions { - readonly botId: string - readonly botName: string - readonly channel: string - readonly redis: Pick, "subscribe"> - readonly heartbeatInterval?: Duration.Input -} - -export const createCommandSseStream = ({ - botId, - botName, - channel, - redis, - heartbeatInterval = HEARTBEAT_INTERVAL, -}: CommandSseStreamOptions) => { - const commandStream = Stream.callback((queue) => - Effect.gen(function* () { - const { unsubscribe } = yield* redis.subscribe(channel, (message) => { - // Encode the message as an SSE event - Queue.offerUnsafe(queue, encodeSseEvent("command", message)) - }) - - // Add finalizer to unsubscribe when stream closes - yield* Effect.addFinalizer(() => - unsubscribe.pipe( - Effect.tap(() => - Effect.logDebug(`Bot ${botId} (${botName}) disconnected from SSE stream`), - ), - Effect.catch(() => Effect.void), - ), - ) - - // Keep the subscription alive until the stream is closed - yield* Effect.never - }).pipe( - Effect.catch((error) => { - // Log the error but don't fail the stream - end it gracefully - Effect.runFork(Effect.logError("Redis subscription error", { error, botId, botName })) - return Queue.end(queue) - }), - ), - ) - - return Stream.merge(commandStream, createSseHeartbeatStream(heartbeatInterval), { - haltStrategy: "either", - }) -} - export const HttpBotCommandsLive = HttpApiBuilder.group(HazelApi, "bot-commands", (handlers) => handlers // SSE stream for bot commands (bot token auth) diff --git a/apps/backend/src/routes/bot-commands.sse.ts b/apps/backend/src/routes/bot-commands.sse.ts new file mode 100644 index 000000000..c6daaf1b0 --- /dev/null +++ b/apps/backend/src/routes/bot-commands.sse.ts @@ -0,0 +1,90 @@ +import { Sse } from "effect/unstable/encoding" +import { Duration, Effect, Queue, Schedule, Stream } from "effect" + +const HEARTBEAT_INTERVAL = "25 seconds" as const + +const encodeSseEvent = (event: string, data: string) => + Sse.encoder.write({ + _tag: "Event", + event, + id: undefined, + data, + }) + +export type CommandSseRedis = { + readonly subscribe: ( + channel: string, + handler: (message: string, channel: string) => void, + ) => Effect.Effect<{ + readonly unsubscribe: Effect.Effect + }, unknown> +} + +export const createSseHeartbeatStream = (interval: Duration.Input = HEARTBEAT_INTERVAL) => + Stream.make( + encodeSseEvent( + "heartbeat", + JSON.stringify({ + type: "heartbeat", + timestamp: Date.now(), + }), + ), + ).pipe( + Stream.concat( + Stream.fromSchedule(Schedule.spaced(interval)).pipe( + Stream.map(() => + encodeSseEvent( + "heartbeat", + JSON.stringify({ + type: "heartbeat", + timestamp: Date.now(), + }), + ), + ), + ), + ), + ) + +interface CommandSseStreamOptions { + readonly botId: string + readonly botName: string + readonly channel: string + readonly redis: CommandSseRedis + readonly heartbeatInterval?: Duration.Input +} + +export const createCommandSseStream = ({ + botId, + botName, + channel, + redis, + heartbeatInterval = HEARTBEAT_INTERVAL, +}: CommandSseStreamOptions) => { + const commandStream = Stream.callback((queue) => + Effect.gen(function* () { + const { unsubscribe } = yield* redis.subscribe(channel, (message) => { + Queue.offerUnsafe(queue, encodeSseEvent("command", message)) + }) + + yield* Effect.addFinalizer(() => + unsubscribe.pipe( + Effect.tap(() => + Effect.logDebug(`Bot ${botId} (${botName}) disconnected from SSE stream`), + ), + Effect.catch(() => Effect.void), + ), + ) + + yield* Effect.never + }).pipe( + Effect.catch((error) => { + Effect.runFork(Effect.logError("Redis subscription error", { error, botId, botName })) + return Queue.end(queue) + }), + ), + ) + + return Stream.merge(commandStream, createSseHeartbeatStream(heartbeatInterval), { + haltStrategy: "either", + }) +} diff --git a/apps/backend/src/routes/chat-sync.http.ts b/apps/backend/src/routes/chat-sync.http.ts index e2490d453..1e049b833 100644 --- a/apps/backend/src/routes/chat-sync.http.ts +++ b/apps/backend/src/routes/chat-sync.http.ts @@ -172,11 +172,6 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han toInternalServerError("Database error while deleting sync connection", error), ), ), - Effect.catchTag("SchemaError", (error) => - Effect.fail( - toInternalServerError("Schema error while deleting sync connection", error), - ), - ), ), ) .handle("createChannelLink", ({ params, payload }) => @@ -303,9 +298,6 @@ export const HttpChatSyncLive = HttpApiBuilder.group(HazelApi, "chat-sync", (han toInternalServerError("Database error while deleting channel link", error), ), ), - Effect.catchTag("SchemaError", (error) => - Effect.fail(toInternalServerError("Schema error while deleting channel link", error)), - ), ), ) }), diff --git a/apps/backend/src/routes/integration-resources.http.ts b/apps/backend/src/routes/integration-resources.http.ts index 70121afc4..7da96b359 100644 --- a/apps/backend/src/routes/integration-resources.http.ts +++ b/apps/backend/src/routes/integration-resources.http.ts @@ -341,6 +341,13 @@ export const HttpIntegrationResourceLive = HttpApiBuilder.group( detail: String(error), }), ), + ConfigError: (error) => + Effect.fail( + new InternalServerError({ + message: "Discord bot token not configured", + detail: String(error), + }), + ), }), ), ), @@ -470,7 +477,7 @@ const handleGetDiscordGuildChannels = Effect.fn("integration-resources.getDiscor ) return new DiscordGuildChannelsResponse({ - channels: channels.map((c: { id: string; name: string; type: number }) => ({ + channels: channels.map((c) => ({ ...c, id: c.id as ExternalChannelId, })), diff --git a/apps/backend/src/routes/integrations.http.test.ts b/apps/backend/src/routes/integrations.http.test.ts index 021ec3c7a..ece39ad3d 100644 --- a/apps/backend/src/routes/integrations.http.test.ts +++ b/apps/backend/src/routes/integrations.http.test.ts @@ -69,7 +69,7 @@ describe("integration connect API key helpers", () => { expect(Result.isSuccess(result)).toBe(true) if (Result.isSuccess(result)) { - expect(result.value).toBe("https://connect.craft.do/links/link_123/api/v1") + expect(result.success).toBe("https://connect.craft.do/links/link_123/api/v1") } }) diff --git a/apps/backend/src/routes/integrations.http.ts b/apps/backend/src/routes/integrations.http.ts index 2a220cef6..99182ae3c 100644 --- a/apps/backend/src/routes/integrations.http.ts +++ b/apps/backend/src/routes/integrations.http.ts @@ -267,7 +267,7 @@ const makeOAuthSessionCookie = ( }) const expireOAuthSessionCookie = (name: string, options: { cookieDomain: string; secure: boolean }) => - HttpServerResponse.expireCookie(name, { + HttpServerResponse.expireCookieUnsafe(name, { domain: options.cookieDomain, path: "/", httpOnly: true, @@ -1225,14 +1225,6 @@ export const HttpIntegrationLive = HttpApiBuilder.group(HazelApi, "integrations" }), ), ), - Effect.catchTag("SchemaError", (error) => - Effect.fail( - new InternalServerError({ - message: "Schema error during OAuth callback", - detail: String(error), - }), - ), - ), Effect.catchTag("ConfigError", (err) => Effect.fail( new InternalServerError({ message: "Missing configuration", detail: String(err) }), diff --git a/apps/backend/src/rpc/handlers/channels.ts b/apps/backend/src/rpc/handlers/channels.ts index 5be85d373..bc20c0d21 100644 --- a/apps/backend/src/rpc/handlers/channels.ts +++ b/apps/backend/src/rpc/handlers/channels.ts @@ -478,7 +478,14 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( const originalMessageId = originalMessageResult[0]!.id - const clusterUrl = yield* Config.string("CLUSTER_URL") + const clusterUrl = yield* Config.string("CLUSTER_URL").asEffect().pipe( + Effect.mapError(() => + new WorkflowServiceUnavailableError({ + message: "CLUSTER_URL not configured", + cause: "Missing CLUSTER_URL environment variable", + }), + ), + ) const client = yield* HttpApiClient.make(Cluster.WorkflowApi, { baseUrl: clusterUrl, }) @@ -508,6 +515,14 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( }), ), ), + Effect.catchTag("BadRequest", (err) => + Effect.fail( + new InternalServerError({ + message: "Failed to trigger thread naming workflow", + cause: String(err), + }), + ), + ), Effect.catchTag("SchemaError", (err) => Effect.fail( new InternalServerError({ diff --git a/apps/backend/src/rpc/handlers/connect-shares.test.ts b/apps/backend/src/rpc/handlers/connect-shares.test.ts index edec41d14..43d36307a 100644 --- a/apps/backend/src/rpc/handlers/connect-shares.test.ts +++ b/apps/backend/src/rpc/handlers/connect-shares.test.ts @@ -11,10 +11,10 @@ import { validateInviteAcceptanceTarget, } from "./connect-shares" -const HOST_ORG_ID = "00000000-0000-0000-0000-000000000101" as OrganizationId -const GUEST_ORG_ID = "00000000-0000-0000-0000-000000000102" as OrganizationId -const OTHER_GUEST_ORG_ID = "00000000-0000-0000-0000-000000000103" as OrganizationId -const GUEST_CHANNEL_ID = "00000000-0000-0000-0000-000000000301" as ChannelId +const HOST_ORG_ID = "00000000-0000-4000-8000-000000000101" as OrganizationId +const GUEST_ORG_ID = "00000000-0000-4000-8000-000000000102" as OrganizationId +const OTHER_GUEST_ORG_ID = "00000000-0000-4000-8000-000000000103" as OrganizationId +const GUEST_CHANNEL_ID = "00000000-0000-4000-8000-000000000301" as ChannelId describe("connect-shares helpers", () => { it("rejects invite creation when provided org and slug resolve to different workspaces", async () => { diff --git a/apps/backend/src/rpc/handlers/organizations.ts b/apps/backend/src/rpc/handlers/organizations.ts index 0c2fb62e7..863a6c684 100644 --- a/apps/backend/src/rpc/handlers/organizations.ts +++ b/apps/backend/src/rpc/handlers/organizations.ts @@ -1,4 +1,5 @@ import { GeneratePortalLinkIntent } from "@workos-inc/node" +import type { OrganizationId } from "@hazel/schema" import { ChannelMemberRepo, ChannelRepo, @@ -21,6 +22,8 @@ import { OrganizationPolicy } from "../../policies/organization-policy" import { ChannelAccessSyncService } from "../../services/channel-access-sync" import { WorkOSAuth as WorkOS } from "../../services/workos-auth" +const UNKNOWN_ORGANIZATION_ID = "00000000-0000-4000-8000-000000000000" as OrganizationId + /** * Custom error handler for organization database operations that provides * specific error handling for duplicate slug violations @@ -41,7 +44,10 @@ const handleOrganizationDbErrors = ( return effect.pipe( Effect.catchIf( (e): e is Extract => Predicate.isTagged(e, "DatabaseError"), - (err) => { + (err): Effect.Effect< + never, + InternalServerError | OrganizationSlugAlreadyExistsError + > => { const dbErr = err as unknown as { type: string cause: { constraint_name?: string; detail?: string } @@ -382,7 +388,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( if (Option.isNone(orgOption)) { return yield* new OrganizationNotFoundError({ - organizationId: "unknown" as any, + organizationId: UNKNOWN_ORGANIZATION_ID, }) } diff --git a/apps/backend/src/rpc/handlers/users.ts b/apps/backend/src/rpc/handlers/users.ts index 57086bf83..95a3c86f4 100644 --- a/apps/backend/src/rpc/handlers/users.ts +++ b/apps/backend/src/rpc/handlers/users.ts @@ -15,7 +15,7 @@ export const UserRpcLive = UserRpcs.toLayer( const userRepo = yield* UserRepo return { - "user.me": () => CurrentUser.Context, + "user.me": () => CurrentUser.Context.asEffect(), "user.update": ({ id, ...payload }) => db diff --git a/apps/backend/src/rpc/middleware/auth.test.ts b/apps/backend/src/rpc/middleware/auth.test.ts index 6edc5e5f8..ecc79022f 100644 --- a/apps/backend/src/rpc/middleware/auth.test.ts +++ b/apps/backend/src/rpc/middleware/auth.test.ts @@ -1,22 +1,38 @@ -import { describe, expect, it, layer } from "@effect/vitest" +import { describe, expect, it } from "@effect/vitest" import { Headers } from "effect/unstable/http" -import { BotRepo, UserPresenceStatusRepo, UserRepo } from "@hazel/backend-core" -import { Effect, Exit, Layer, Option, FiberRef } from "effect" -import { AuthMiddleware } from "./auth-class.ts" -import { SessionManager } from "../../services/session-manager.ts" -import type { CurrentUser } from "@hazel/domain" -import { SessionExpiredError, InvalidJwtPayloadError, InvalidBearerTokenError } from "@hazel/domain" +import type { SuccessValue } from "effect/unstable/rpc/RpcMiddleware" +import { BotRepo, UserRepo } from "@hazel/backend-core" +import { + CurrentUser, + type CurrentUser as CurrentUserNamespace, +} from "@hazel/domain" import type { UserId } from "@hazel/schema" - -// ===== Mock CurrentUser Factory ===== - -const createMockCurrentUser = (overrides?: Partial): CurrentUser.Schema => ({ - id: "usr_test123" as UserId, +import { Effect, Layer, Option, Ref, Result, ServiceMap } from "effect" +import { AuthMiddleware, AuthMiddlewareLive } from "./auth.ts" +import { SessionManager } from "../../services/session-manager.ts" +import { serviceShape } from "../../test/effect-helpers" + +const USER_ID = "00000000-0000-4000-8000-000000000001" as UserId +const BOT_USER_ID = "00000000-0000-4000-8000-000000000002" as UserId + +type SessionManagerShape = ServiceMap.Service.Shape +type BotRepoShape = ServiceMap.Service.Shape +type UserRepoShape = ServiceMap.Service.Shape +type EffectSuccess = T extends Effect.Effect ? A : never +type OptionValue = T extends Option.Option ? A : never +type BotRecord = OptionValue>> +type UserRecord = OptionValue>> +const successValue = { _: Symbol("success") } as SuccessValue + +const makeCurrentUser = ( + overrides: Partial = {}, +): CurrentUserNamespace.Schema => ({ + id: USER_ID, email: "test@example.com", firstName: "Test", lastName: "User", avatarUrl: "https://example.com/avatar.png", - role: "member" as const, + role: "member", isOnboarded: true, timezone: "UTC", organizationId: null, @@ -24,304 +40,190 @@ const createMockCurrentUser = (overrides?: Partial): Current ...overrides, }) -// ===== Mock RPC Context ===== -// Helper to create the full middleware context -const createMiddlewareContext = (headers: Headers.Headers) => ({ - clientId: 1, - rpc: {} as any, // Mock RPC definition - payload: {}, - headers, -}) - -// ===== Mock SessionManager Factory ===== - -const createMockSessionManagerLive = (options?: { - currentUser?: CurrentUser.Schema - shouldFail?: Effect.Effect -}) => - Layer.succeed(SessionManager, { - authenticateWithBearer: (_token: string) => { - if (options?.shouldFail) { - return options.shouldFail - } - return Effect.succeed(options?.currentUser ?? createMockCurrentUser()) - }, - } as unknown as SessionManager) - -// ===== Mock UserPresenceStatusRepo ===== - -const createMockPresenceRepoLive = (options?: { onUpdateStatus?: (params: any) => void }) => - Layer.succeed(UserPresenceStatusRepo, { - updateStatus: (params: any) => { - options?.onUpdateStatus?.(params) - return Effect.void - }, - findByUserId: (_userId: string) => Effect.succeed(Option.none()), - } as unknown as UserPresenceStatusRepo) - -// ===== Mock BotRepo ===== - -const MockBotRepoLive = Layer.succeed(BotRepo, { - findByTokenHash: (_hash: string) => Effect.succeed(Option.none()), -} as unknown as BotRepo) - -// ===== Mock UserRepo ===== - -const MockUserRepoLive = Layer.succeed(UserRepo, { - findById: (_id: string) => Effect.succeed(Option.none()), -} as unknown as UserRepo) - -// ===== Auth Middleware Layer Factory ===== - -const makeAuthMiddlewareLayer = (options?: { - sessionManagerLayer?: Layer.Layer - presenceRepoLayer?: Layer.Layer -}): Layer.Layer => { - const sessionManagerLayer = options?.sessionManagerLayer ?? createMockSessionManagerLive() - const presenceRepoLayer = options?.presenceRepoLayer ?? createMockPresenceRepoLive() - - return Layer.effect( - AuthMiddleware, - Effect.gen(function* () { - const sessionManager = yield* SessionManager - const presenceRepo = yield* UserPresenceStatusRepo - - // Create a FiberRef to track the current user - const currentUserRef = yield* FiberRef.make>(Option.none()) - - // Add finalizer - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - const userOption = yield* FiberRef.get(currentUserRef) - if (Option.isSome(userOption)) { - yield* ( - presenceRepo.updateStatus({ - userId: userOption.value.id, - status: "offline", - customMessage: null, - }) as unknown as Effect.Effect - ).pipe(Effect.catch(() => Effect.void)) - } - }), - ) +const hashToken = async (token: string) => { + const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token)) + return Array.from(new Uint8Array(digest)) + .map((value) => value.toString(16).padStart(2, "0")) + .join("") +} - return AuthMiddleware.of(({ headers }) => - Effect.gen(function* () { - // Require Bearer token - const authHeader = Headers.get(headers, "authorization") - if (Option.isNone(authHeader) || !authHeader.value.startsWith("Bearer ")) { - return yield* new InvalidBearerTokenError({ - message: "No Bearer token provided", - detail: "Authentication requires a Bearer token", - }) - } +const invokeMiddleware = (headers: Headers.Headers) => + Effect.gen(function* () { + const currentUserRef = yield* Ref.make>(Option.none()) + const middleware = yield* AuthMiddleware + yield* middleware( + Effect.gen(function* () { + const currentUser = yield* CurrentUser.Context + yield* Ref.set(currentUserRef, Option.some(currentUser)) + return successValue + }), + { + clientId: 1, + requestId: 1n as never, + rpc: {} as never, + payload: undefined, + headers, + }, + ) + return yield* Ref.get(currentUserRef) + }) - const token = authHeader.value.slice(7) - const currentUser = yield* sessionManager.authenticateWithBearer(token) +const makeSessionManagerLayer = (currentUser: CurrentUserNamespace.Schema) => + Layer.succeed( + SessionManager, + serviceShape({ + authenticateWithBearer: () => Effect.succeed(currentUser), + }), + ) - // Store user in FiberRef - yield* FiberRef.set(currentUserRef, Option.some(currentUser)) +const makeBotRepoLayer = (findByTokenHash: BotRepoShape["findByTokenHash"]) => + Layer.succeed( + BotRepo, + serviceShape({ + findByTokenHash, + }), + ) - return currentUser - }), - ) +const makeUserRepoLayer = (findById: UserRepoShape["findById"]) => + Layer.succeed( + UserRepo, + serviceShape({ + findById, }), - ).pipe( - Layer.provide(sessionManagerLayer), - Layer.provide(presenceRepoLayer), - Layer.provide(MockBotRepoLive), - Layer.provide(MockUserRepoLive), - ) as Layer.Layer + ) + +const BOT_RECORD: BotRecord = { + id: "00000000-0000-4000-8000-000000000010" as BotRecord["id"], + userId: BOT_USER_ID, + createdBy: USER_ID, + name: "Test Bot", + description: null, + webhookUrl: null, + apiTokenHash: "token-hash", + scopes: ["messages:read"], + metadata: null, + isPublic: false, + installCount: 0, + allowedIntegrations: null, + mentionable: true, + createdAt: new Date("2026-01-01T00:00:00.000Z"), + updatedAt: new Date("2026-01-01T00:00:00.000Z"), + deletedAt: null, } -// Default test layer -const TestAuthMiddlewareLive = makeAuthMiddlewareLayer() - -// ===== Tests ===== - -describe("AuthMiddleware", () => { - describe("bearer token extraction", () => { - layer(TestAuthMiddlewareLive)("bearer parsing", (it) => { - it.scoped("parses Bearer token from Authorization header", () => - Effect.gen(function* () { - const middleware = yield* AuthMiddleware - const headers = Headers.fromInput({ - authorization: "Bearer valid-bearer-token", - }) - - const result = yield* middleware(createMiddlewareContext(headers)) +const USER_RECORD: UserRecord = { + id: BOT_USER_ID, + externalId: "external-user-id", + email: "bot@example.com", + firstName: "Hazel", + lastName: "Bot", + avatarUrl: null, + userType: "machine", + settings: null, + isOnboarded: true, + timezone: "UTC", + createdAt: new Date("2026-01-01T00:00:00.000Z"), + updatedAt: new Date("2026-01-01T00:00:00.000Z"), + deletedAt: null, +} - expect(result.id).toBe("usr_test123") - expect(result.email).toBe("test@example.com") - }), - ) +const runAuth = ( + headers: Headers.Headers, + overrides: { + sessionManager?: ReturnType + botRepo?: ReturnType + userRepo?: ReturnType + } = {}, +) => + Effect.runPromise( + Effect.scoped( + invokeMiddleware(headers).pipe( + Effect.provide(AuthMiddlewareLive), + Effect.provide(overrides.sessionManager ?? makeSessionManagerLayer(makeCurrentUser())), + Effect.provide( + overrides.botRepo ?? + makeBotRepoLayer(() => Effect.succeed(Option.none())), + ), + Effect.provide( + overrides.userRepo ?? + makeUserRepoLayer(() => Effect.succeed(Option.none())), + ), + Effect.result, + ), + ), + ) + +describe("AuthMiddlewareLive", () => { + it("authenticates JWT bearer tokens through SessionManager", async () => { + const currentUser = makeCurrentUser({ email: "jwt@example.com" }) + const result = await runAuth(Headers.fromInput({ authorization: "Bearer a.b.c" }), { + sessionManager: makeSessionManagerLayer(currentUser), }) - layer(TestAuthMiddlewareLive)("missing token", (it) => { - it.scoped("fails with InvalidBearerTokenError when Authorization header is missing", () => - Effect.gen(function* () { - const middleware = yield* AuthMiddleware - const headers = Headers.fromInput({}) - - const exit = yield* middleware(createMiddlewareContext(headers)).pipe(Effect.exit) - - expect(Exit.isFailure(exit)).toBe(true) - }), - ) - - it.scoped("fails when Authorization header has no Bearer prefix", () => - Effect.gen(function* () { - const middleware = yield* AuthMiddleware - const headers = Headers.fromInput({ - authorization: "Basic some-credentials", - }) - - const exit = yield* middleware(createMiddlewareContext(headers)).pipe(Effect.exit) - - expect(Exit.isFailure(exit)).toBe(true) - }), - ) - }) + expect(Result.isSuccess(result)).toBe(true) + if (Result.isSuccess(result)) { + expect(Option.isSome(result.success)).toBe(true) + if (Option.isSome(result.success)) { + expect(result.success.value).toEqual(currentUser) + } + } }) - describe("session validation", () => { - layer(TestAuthMiddlewareLive)("valid session", (it) => { - it.scoped("returns CurrentUser on successful validation", () => - Effect.gen(function* () { - const middleware = yield* AuthMiddleware - const headers = Headers.fromInput({ - authorization: "Bearer valid-token", - }) - - const result = yield* middleware(createMiddlewareContext(headers)) - - expect(result.id).toBe("usr_test123") - expect(result.email).toBe("test@example.com") - expect(result.role).toBe("member") - expect(result.isOnboarded).toBe(true) - }), - ) - }) - - describe("error propagation", () => { - const expiredTokenLayer = makeAuthMiddlewareLayer({ - sessionManagerLayer: createMockSessionManagerLive({ - shouldFail: Effect.fail( - new SessionExpiredError({ - message: "Token expired", - detail: "Could not verify", - }), - ), - }), - }) - - layer(expiredTokenLayer)("expired token", (it) => { - it.scoped("propagates SessionExpiredError from SessionManager", () => - Effect.gen(function* () { - const middleware = yield* AuthMiddleware - const headers = Headers.fromInput({ - authorization: "Bearer expired-token", - }) - - const exit = yield* middleware(createMiddlewareContext(headers)).pipe(Effect.exit) - - expect(Exit.isFailure(exit)).toBe(true) - }), - ) - }) - - const invalidJwtLayer = makeAuthMiddlewareLayer({ - sessionManagerLayer: createMockSessionManagerLive({ - shouldFail: Effect.fail( - new InvalidJwtPayloadError({ - message: "Invalid JWT", - detail: "Malformed token", - }), - ), - }), - }) - - layer(invalidJwtLayer)("invalid JWT", (it) => { - it.scoped("propagates InvalidJwtPayloadError from SessionManager", () => - Effect.gen(function* () { - const middleware = yield* AuthMiddleware - const headers = Headers.fromInput({ - authorization: "Bearer invalid-jwt", - }) - - const exit = yield* middleware(createMiddlewareContext(headers)).pipe(Effect.exit) - - expect(Exit.isFailure(exit)).toBe(true) - }), - ) - }) - }) + it("fails when no bearer token is provided", async () => { + const result = await runAuth(Headers.fromInput({})) + + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { + const failureTag = + typeof result.failure === "object" && + result.failure !== null && + "_tag" in result.failure + ? result.failure._tag + : "unhandled" + expect(failureTag).toBe("SessionNotProvidedError") + } }) - describe("user context", () => { - const customUser = createMockCurrentUser({ - id: "usr_custom_user" as UserId, - email: "custom@example.com", - organizationId: "org_123" as any, - role: "admin", - }) - - const customUserLayer = makeAuthMiddlewareLayer({ - sessionManagerLayer: createMockSessionManagerLive({ - currentUser: customUser, - }), + it("authenticates bot bearer tokens through BotRepo and UserRepo", async () => { + const token = "bot-token" + const tokenHash = await hashToken(token) + const result = await runAuth(Headers.fromInput({ authorization: `Bearer ${token}` }), { + botRepo: makeBotRepoLayer((candidateHash) => + Effect.succeed( + candidateHash === tokenHash ? Option.some(BOT_RECORD) : Option.none(), + ), + ), + userRepo: makeUserRepoLayer((id) => + Effect.succeed( + id === BOT_USER_ID ? Option.some(USER_RECORD) : Option.none(), + ), + ), }) - layer(customUserLayer)("custom user data", (it) => { - it.scoped("includes organization context when present", () => - Effect.gen(function* () { - const middleware = yield* AuthMiddleware - const headers = Headers.fromInput({ - authorization: "Bearer org-token", - }) - - const result = yield* middleware(createMiddlewareContext(headers)) - - expect(result.organizationId).toBe("org_123") - expect(result.role).toBe("admin") - }), - ) - - it.scoped("includes all user fields from session", () => - Effect.gen(function* () { - const middleware = yield* AuthMiddleware - const headers = Headers.fromInput({ - authorization: "Bearer full-data-token", - }) - - const result = yield* middleware(createMiddlewareContext(headers)) - - expect(result.id).toBe("usr_custom_user") - expect(result.email).toBe("custom@example.com") - expect(result.firstName).toBe("Test") - expect(result.lastName).toBe("User") - expect(result.avatarUrl).toBe("https://example.com/avatar.png") - }), - ) - }) + expect(Result.isSuccess(result)).toBe(true) + if (Result.isSuccess(result)) { + expect(Option.isSome(result.success)).toBe(true) + if (Option.isSome(result.success)) { + expect(result.success.value.id).toBe(BOT_USER_ID) + expect(result.success.value.email).toBe("bot@example.com") + expect(result.success.value.firstName).toBe("Hazel") + expect(result.success.value.lastName).toBe("Bot") + } + } }) - describe("FiberRef tracking", () => { - layer(TestAuthMiddlewareLive)("user tracking", (it) => { - it.scoped("successfully authenticates and returns user", () => - Effect.gen(function* () { - const middleware = yield* AuthMiddleware - const headers = Headers.fromInput({ - authorization: "Bearer tracked-user-token", - }) - - const result = yield* middleware(createMiddlewareContext(headers)) - - // Verify user is returned correctly - expect(result.id).toBe("usr_test123") - expect(result.email).toBe("test@example.com") - }), - ) - }) + it("fails for invalid bot bearer tokens", async () => { + const result = await runAuth(Headers.fromInput({ authorization: "Bearer invalid-bot-token" })) + + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { + const failureTag = + typeof result.failure === "object" && + result.failure !== null && + "_tag" in result.failure + ? result.failure._tag + : "unhandled" + expect(failureTag).toBe("InvalidBearerTokenError") + } }) }) diff --git a/apps/backend/src/services/bot-gateway-service.test.ts b/apps/backend/src/services/bot-gateway-service.test.ts index fd12431f9..ad1828e07 100644 --- a/apps/backend/src/services/bot-gateway-service.test.ts +++ b/apps/backend/src/services/bot-gateway-service.test.ts @@ -1,29 +1,31 @@ import { describe, expect, it } from "@effect/vitest" import { BotInstallationRepo, ChannelRepo } from "@hazel/backend-core" import { createBotGatewayPartitionKey } from "@hazel/domain" -import type { BotId, ChannelId, OrganizationId, UserId } from "@hazel/schema" -import { Effect, Layer, Option, ServiceMap } from "effect" -import { buildServiceLayer, configLayer } from "../test/effect-helpers" +import { Message } from "@hazel/domain/models" +import type { BotId, ChannelId, MessageId, OrganizationId, UserId } from "@hazel/schema" +import { Effect, Layer, Option, Schema } from "effect" +import { configLayer, serviceShape } from "../test/effect-helpers" import { BotGatewayService } from "./bot-gateway-service" const DURABLE_STREAMS_URL = "http://durable.test/v1/stream" -const BOT_ID = "00000000-0000-0000-0000-000000000111" as BotId -const SECOND_BOT_ID = "00000000-0000-0000-0000-000000000112" as BotId -const CHANNEL_ID = "00000000-0000-0000-0000-000000000444" as ChannelId -const ORG_ID = "00000000-0000-0000-0000-000000000333" as OrganizationId -const USER_ID = "00000000-0000-0000-0000-000000000222" as UserId +const BOT_ID = "00000000-0000-4000-8000-000000000111" as BotId +const SECOND_BOT_ID = "00000000-0000-4000-8000-000000000112" as BotId +const CHANNEL_ID = "00000000-0000-4000-8000-000000000444" as ChannelId +const ORG_ID = "00000000-0000-4000-8000-000000000333" as OrganizationId +const USER_ID = "00000000-0000-4000-8000-000000000222" as UserId +const MESSAGE_ID = "00000000-0000-4000-8000-000000000555" as MessageId const TestConfigLive = configLayer({ DURABLE_STREAMS_URL, }) const makeBotInstallationRepoLayer = (botIds: ReadonlyArray) => - Layer.succeed(BotInstallationRepo, { + Layer.succeed(BotInstallationRepo, serviceShape({ getBotIdsForOrg: () => Effect.succeed([...botIds]), - } as ServiceMap.Service.Shape) + })) const makeChannelRepoLayer = (organizationId: OrganizationId) => - Layer.succeed(ChannelRepo, { + Layer.succeed(ChannelRepo, serviceShape({ findById: (id: ChannelId) => Effect.succeed( Option.some({ @@ -31,10 +33,10 @@ const makeChannelRepoLayer = (organizationId: OrganizationId) => organizationId, }), ), - } as ServiceMap.Service.Shape) + })) const makeServiceLayer = (botIds: ReadonlyArray) => - buildServiceLayer(BotGatewayService).pipe( + Layer.effect(BotGatewayService, BotGatewayService.make).pipe( Layer.provide(makeBotInstallationRepoLayer(botIds)), Layer.provide(makeChannelRepoLayer(ORG_ID)), Layer.provide(TestConfigLive), @@ -100,6 +102,19 @@ describe("BotGatewayService", () => { it("fans message events out to every bot installed in the organization", () => { const originalFetch = globalThis.fetch const requests: Array<{ url: string; method: string; body: string | null }> = [] + const message = { + id: MESSAGE_ID, + channelId: CHANNEL_ID, + conversationId: null, + authorId: USER_ID, + content: "hello from hazel", + embeds: null, + replyToMessageId: null, + threadChannelId: null, + createdAt: new Date("2026-03-05T12:00:00.000Z"), + updatedAt: null, + deletedAt: null, + } satisfies Schema.Schema.Type globalThis.fetch = (async (input, init) => { requests.push({ @@ -113,12 +128,7 @@ describe("BotGatewayService", () => { return Effect.runPromise( Effect.gen(function* () { const gateway = yield* BotGatewayService - yield* gateway.publishMessageEvent("message.create", { - id: "00000000-0000-0000-0000-000000000555", - channelId: CHANNEL_ID, - createdAt: new Date("2026-03-05T12:00:00.000Z"), - updatedAt: null, - } as any) + yield* gateway.publishMessageEvent("message.create", message) const appendRequests = requests.filter((request) => request.method === "POST") expect(appendRequests).toHaveLength(2) diff --git a/apps/backend/src/services/chat-sync/chat-sync-attachment-content.test.ts b/apps/backend/src/services/chat-sync/chat-sync-attachment-content.test.ts index bd0c9ccb3..6ab6ae6f0 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-attachment-content.test.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-attachment-content.test.ts @@ -43,13 +43,13 @@ describe("formatMessageContentWithAttachments", () => { content: "release notes", attachments: [ makeAttachment({ - id: "00000000-0000-0000-0000-000000000001", + id: "00000000-0000-4000-8000-000000000001", fileName: "design.png", fileSize: 2048, publicUrl: "https://cdn.example.com/1", }), makeAttachment({ - id: "00000000-0000-0000-0000-000000000002", + id: "00000000-0000-4000-8000-000000000002", fileName: "quarterly-report.pdf", fileSize: 1024 * 1024, publicUrl: "https://cdn.example.com/2", @@ -66,13 +66,13 @@ describe("formatMessageContentWithAttachments", () => { it("renders deterministically based on provided attachment order", () => { const attachments = [ makeAttachment({ - id: "00000000-0000-0000-0000-0000000000b0", + id: "00000000-0000-4000-8000-0000000000b0", fileName: "b.txt", fileSize: 1, publicUrl: "https://cdn.example.com/b", }), makeAttachment({ - id: "00000000-0000-0000-0000-0000000000a0", + id: "00000000-0000-4000-8000-0000000000a0", fileName: "a.txt", fileSize: 1, publicUrl: "https://cdn.example.com/a", @@ -89,7 +89,7 @@ describe("formatMessageContentWithAttachments", () => { it("caps long attachment lists and includes summary line", () => { const attachments = Array.from({ length: 20 }, (_, index) => makeAttachment({ - id: `00000000-0000-0000-0000-0000000000${String(index).padStart(2, "0")}`, + id: `00000000-0000-4000-8000-0000000000${String(index).padStart(2, "0")}`, fileName: `file-${index}.txt`, fileSize: 10, publicUrl: `https://cdn.example.com/${index}`, diff --git a/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.test.ts b/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.test.ts index 7e4888ded..ce9d47049 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.test.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.test.ts @@ -3,25 +3,22 @@ import { MessageRepo, OrganizationMemberRepo, UserRepo } from "@hazel/backend-co import type { OrganizationId, UserId } from "@hazel/schema" import { Effect, Layer, ServiceMap } from "effect" import { ChatSyncAttributionReconciler } from "./chat-sync-attribution-reconciler.ts" -import { buildServiceLayer, serviceEffect, serviceShape } from "../../test/effect-helpers" +import { serviceShape } from "../../test/effect-helpers" -const ORGANIZATION_ID = "00000000-0000-0000-0000-000000000001" as OrganizationId -const USER_ID = "00000000-0000-0000-0000-000000000002" as UserId -const SHADOW_USER_ID = "00000000-0000-0000-0000-000000000003" as UserId +const ORGANIZATION_ID = "00000000-0000-4000-8000-000000000001" as OrganizationId +const USER_ID = "00000000-0000-4000-8000-000000000002" as UserId +const SHADOW_USER_ID = "00000000-0000-4000-8000-000000000003" as UserId const makeLayer = (deps: { - messageRepo: MessageRepo - userRepo: UserRepo - organizationMemberRepo: OrganizationMemberRepo + messageRepo: ServiceMap.Service.Shape + userRepo: ServiceMap.Service.Shape + organizationMemberRepo: ServiceMap.Service.Shape }) => - buildServiceLayer(ChatSyncAttributionReconciler).pipe( - Layer.provide(Layer.succeed(MessageRepo, deps.messageRepo as ServiceMap.Service.Shape)), - Layer.provide(Layer.succeed(UserRepo, deps.userRepo as ServiceMap.Service.Shape)), + Layer.effect(ChatSyncAttributionReconciler, ChatSyncAttributionReconciler.make).pipe( + Layer.provide(Layer.succeed(MessageRepo, deps.messageRepo)), + Layer.provide(Layer.succeed(UserRepo, deps.userRepo)), Layer.provide( - Layer.succeed( - OrganizationMemberRepo, - deps.organizationMemberRepo as ServiceMap.Service.Shape, - ), + Layer.succeed(OrganizationMemberRepo, deps.organizationMemberRepo), ), ) @@ -45,13 +42,13 @@ describe("ChatSyncAttributionReconciler", () => { }) const result = await Effect.runPromise( - serviceEffect(ChatSyncAttributionReconciler, (service) => + ChatSyncAttributionReconciler.use((service) => service.relinkHistoricalProviderMessages({ - organizationId: ORGANIZATION_ID, - provider: "discord", - userId: USER_ID, - externalAccountId: "123", - externalAccountName: "Maki", + organizationId: ORGANIZATION_ID, + provider: "discord", + userId: USER_ID, + externalAccountId: "123", + externalAccountName: "Maki", }), ).pipe(Effect.provide(layer)), ) @@ -84,13 +81,13 @@ describe("ChatSyncAttributionReconciler", () => { }) const result = await Effect.runPromise( - serviceEffect(ChatSyncAttributionReconciler, (service) => + ChatSyncAttributionReconciler.use((service) => service.unlinkHistoricalProviderMessages({ - organizationId: ORGANIZATION_ID, - provider: "discord", - userId: USER_ID, - externalAccountId: "123", - externalAccountName: "Maki", + organizationId: ORGANIZATION_ID, + provider: "discord", + userId: USER_ID, + externalAccountId: "123", + externalAccountName: "Maki", }), ).pipe(Effect.provide(layer)), ) diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index 03f012469..89bbe8f9b 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -1,5 +1,4 @@ import { createHash } from "node:crypto" -import type { DiscordSyncWorker as DiscordSyncWorkerType } from "./discord-sync-worker" import { and, asc, Database, eq, isNull, schema } from "@hazel/db" import { ChannelRepo, @@ -151,12 +150,20 @@ export interface ChatSyncIngressThreadCreate { readonly dedupeKey?: string } -export class ChatSyncCoreWorker extends ServiceMap.Service()("ChatSyncCoreWorker", { - make: Effect.suspend((): Effect.Effect> => ChatSyncCoreWorkerMake), -}) {} +// Tag-only service: real implementation is provided via ChatSyncCoreWorkerMake + ChatSyncCoreWorkerLayer. +// Uses two-type-param overload to avoid requiring `make` (which would cause circular inference with DiscordSyncWorker). +// Shape uses `Record` because the real shape can't be inferred without circular deps. +// Consumers can access methods via `yield* ChatSyncCoreWorker` — method calls are checked at each call site. +export class ChatSyncCoreWorker extends ServiceMap.Service< + ChatSyncCoreWorker, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + Record +>()("ChatSyncCoreWorker") {} /** @internal — exported for integration tests that provide their own deps */ -export const ChatSyncCoreWorkerMake = Effect.gen(function* () { +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +export const ChatSyncCoreWorkerMake: Effect.Effect, unknown, unknown> = + Effect.gen(function* () { const db = yield* Database.Database const connectionRepo = yield* ChatSyncConnectionRepo const channelLinkRepo = yield* ChatSyncChannelLinkRepo @@ -173,8 +180,6 @@ export const ChatSyncCoreWorkerMake = Effect.gen(function* () { const channelAccessSyncService = yield* ChannelAccessSyncService const providerRegistry = yield* ChatSyncProviderRegistry const discordApiClient = yield* Discord.DiscordApiClient - const { DiscordSyncWorker } = yield* Effect.promise(() => import("./discord-sync-worker")) - const discordSyncWorker = yield* DiscordSyncWorker as typeof DiscordSyncWorkerType const payloadHash = (value: unknown): string => createHash("sha256").update(JSON.stringify(value)).digest("hex") @@ -365,7 +370,7 @@ export const ChatSyncCoreWorkerMake = Effect.gen(function* () { const isWebhookStrategyEnabled = ( outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, - ): boolean => outboundIdentity.strategy === "webhook" + ): boolean => outboundIdentity.enabled && outboundIdentity.strategy === "webhook" const defaultOutboundIdentitySettings = (): ChatSyncChannelLink.OutboundIdentitySettings => ({ enabled: false, diff --git a/apps/backend/src/services/chat-sync/discord-gateway-service.dispatch.test.ts b/apps/backend/src/services/chat-sync/discord-gateway-service.dispatch.test.ts index cbeff4e0a..396ef5fdc 100644 --- a/apps/backend/src/services/chat-sync/discord-gateway-service.dispatch.test.ts +++ b/apps/backend/src/services/chat-sync/discord-gateway-service.dispatch.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@effect/vitest" import type { ExternalChannelId, SyncConnectionId } from "@hazel/schema" -import { Effect } from "effect" +import { Effect, ServiceMap } from "effect" import { recordChatSyncDiagnostic } from "../../test/chat-sync-test-diagnostics" import { createDiscordGatewayDispatchHandlers, @@ -9,7 +9,16 @@ import { type DiscordMessageReactionAddEvent, type DiscordMessageUpdateEvent, type DiscordThreadCreateEvent, -} from "./discord-gateway-service" +} from "./discord-gateway-shared" +import { + DiscordSyncWorker, + type DiscordIngressMessageCreate, + type DiscordIngressMessageDelete, + type DiscordIngressMessageUpdate, + type DiscordIngressReactionAdd, + type DiscordIngressReactionRemove, + type DiscordIngressThreadCreate, +} from "./discord-sync-worker" const run = (effect: Effect.Effect) => Effect.runPromise(effect as Effect.Effect) @@ -19,44 +28,56 @@ type GatewayLink = { direction: "both" | "hazel_to_external" | "external_to_hazel" } +type DispatchWorker = Pick< + ServiceMap.Service.Shape, + | "ingestMessageCreate" + | "ingestMessageUpdate" + | "ingestMessageDelete" + | "ingestReactionAdd" + | "ingestReactionRemove" + | "ingestThreadCreate" +> + const makeDispatchHarness = () => { const linksByChannel = new Map>() const calls = { - create: [] as Array>, - update: [] as Array>, - delete: [] as Array>, - reactionAdd: [] as Array>, - reactionRemove: [] as Array>, - threadCreate: [] as Array>, + create: [] as Array, + update: [] as Array, + delete: [] as Array, + reactionAdd: [] as Array, + reactionRemove: [] as Array, + threadCreate: [] as Array, } - const handlers = createDiscordGatewayDispatchHandlers({ - discordSyncWorker: { - ingestMessageCreate: (payload: any) => + const discordSyncWorker: DispatchWorker = { + ingestMessageCreate: (payload) => Effect.sync(() => { - calls.create.push(payload as Record) + calls.create.push(payload) }), - ingestMessageUpdate: (payload: any) => + ingestMessageUpdate: (payload) => Effect.sync(() => { - calls.update.push(payload as Record) + calls.update.push(payload) }), - ingestMessageDelete: (payload: any) => + ingestMessageDelete: (payload) => Effect.sync(() => { - calls.delete.push(payload as Record) + calls.delete.push(payload) }), - ingestReactionAdd: (payload: any) => + ingestReactionAdd: (payload) => Effect.sync(() => { - calls.reactionAdd.push(payload as Record) + calls.reactionAdd.push(payload) }), - ingestReactionRemove: (payload: any) => + ingestReactionRemove: (payload) => Effect.sync(() => { - calls.reactionRemove.push(payload as Record) + calls.reactionRemove.push(payload) }), - ingestThreadCreate: (payload: any) => + ingestThreadCreate: (payload) => Effect.sync(() => { - calls.threadCreate.push(payload as Record) + calls.threadCreate.push(payload) }), - } as any, + } + + const handlers = createDiscordGatewayDispatchHandlers({ + discordSyncWorker, findActiveLinksByExternalChannel: (externalChannelId) => Effect.succeed(linksByChannel.get(externalChannelId) ?? []), isCurrentBotAuthor: (authorId) => Effect.succeed(authorId === "bot-self"), @@ -77,11 +98,11 @@ describe("DiscordGatewayService dispatch handlers", () => { const channelId = "123456789012345678" as ExternalChannelId harness.setLinks(channelId, [ { - syncConnectionId: "00000000-0000-0000-0000-000000000001" as SyncConnectionId, + syncConnectionId: "00000000-0000-4000-8000-000000000001" as SyncConnectionId, direction: "both", }, { - syncConnectionId: "00000000-0000-0000-0000-000000000002" as SyncConnectionId, + syncConnectionId: "00000000-0000-4000-8000-000000000002" as SyncConnectionId, direction: "hazel_to_external", }, ]) @@ -109,7 +130,7 @@ describe("DiscordGatewayService dispatch handlers", () => { expected: "one inbound dispatch for both-direction link", actual: `${harness.calls.create.length} dispatches`, }) - expect(harness.calls.create[0]?.syncConnectionId).toBe("00000000-0000-0000-0000-000000000001") + expect(harness.calls.create[0]?.syncConnectionId).toBe("00000000-0000-4000-8000-000000000001") expect(harness.calls.create[0]?.dedupeKey).toBe("discord:gateway:create:223456789012345678") }) @@ -131,7 +152,7 @@ describe("DiscordGatewayService dispatch handlers", () => { const channelId = "123456789012345678" as ExternalChannelId harness.setLinks(channelId, [ { - syncConnectionId: "00000000-0000-0000-0000-000000000011" as SyncConnectionId, + syncConnectionId: "00000000-0000-4000-8000-000000000011" as SyncConnectionId, direction: "both", }, ]) @@ -161,7 +182,7 @@ describe("DiscordGatewayService dispatch handlers", () => { const channelId = "123456789012345678" as ExternalChannelId harness.setLinks(channelId, [ { - syncConnectionId: "00000000-0000-0000-0000-000000000021" as SyncConnectionId, + syncConnectionId: "00000000-0000-4000-8000-000000000021" as SyncConnectionId, direction: "external_to_hazel", }, ]) @@ -187,11 +208,11 @@ describe("DiscordGatewayService dispatch handlers", () => { const channelId = "123456789012345678" as ExternalChannelId harness.setLinks(channelId, [ { - syncConnectionId: "00000000-0000-0000-0000-000000000031" as SyncConnectionId, + syncConnectionId: "00000000-0000-4000-8000-000000000031" as SyncConnectionId, direction: "both", }, { - syncConnectionId: "00000000-0000-0000-0000-000000000032" as SyncConnectionId, + syncConnectionId: "00000000-0000-4000-8000-000000000032" as SyncConnectionId, direction: "hazel_to_external", }, ]) @@ -210,7 +231,7 @@ describe("DiscordGatewayService dispatch handlers", () => { await run( harness.handlers.ingestMessageReactionRemoveEvent({ ...reactionEvent, - } as any), + }), ) expect(harness.calls.reactionAdd).toHaveLength(1) @@ -226,7 +247,7 @@ describe("DiscordGatewayService dispatch handlers", () => { const parentChannelId = "123456789012345678" as ExternalChannelId harness.setLinks(parentChannelId, [ { - syncConnectionId: "00000000-0000-0000-0000-000000000041" as SyncConnectionId, + syncConnectionId: "00000000-0000-4000-8000-000000000041" as SyncConnectionId, direction: "both", }, ]) diff --git a/apps/backend/src/services/chat-sync/discord-gateway-service.test.ts b/apps/backend/src/services/chat-sync/discord-gateway-service.test.ts index 7421ea763..736074fbd 100644 --- a/apps/backend/src/services/chat-sync/discord-gateway-service.test.ts +++ b/apps/backend/src/services/chat-sync/discord-gateway-service.test.ts @@ -7,7 +7,7 @@ import { decodeRequiredExternalId, extractReactionAuthor, normalizeDiscordMessageAttachments, -} from "./discord-gateway-service" +} from "./discord-gateway-shared" describe("DiscordGatewayService reaction author extraction", () => { it("prefers member.user for reaction events", () => { diff --git a/apps/backend/src/services/chat-sync/discord-gateway-shared.ts b/apps/backend/src/services/chat-sync/discord-gateway-shared.ts new file mode 100644 index 000000000..4722ac8e9 --- /dev/null +++ b/apps/backend/src/services/chat-sync/discord-gateway-shared.ts @@ -0,0 +1,545 @@ +import { createHash } from "node:crypto" +import { ExternalChannelId, ExternalMessageId, ExternalThreadId, ExternalUserId, ExternalWebhookId, SyncConnectionId } from "@hazel/schema" +import { ServiceMap, Effect, Option, Schema } from "effect" +import { DiscordSyncWorker } from "./discord-sync-worker" +import type { ChatSyncIngressMessageAttachment } from "./chat-sync-core-worker" + +export interface DiscordMessageAuthor { + id?: string + username?: string + global_name?: string | null + discriminator?: string + avatar?: string | null + bot?: boolean +} + +export interface DiscordReadyEvent { + user?: { id?: string } +} + +export interface DiscordMessageCreateEvent { + id?: string + channel_id?: string + content?: string + webhook_id?: string + attachments?: ReadonlyArray + author?: DiscordMessageAuthor + message_reference?: { + message_id?: string + channel_id?: string + } +} + +interface DiscordMessageAttachment { + id?: string + filename?: string + size?: number + url?: string +} + +export interface DiscordMessageUpdateEvent { + id?: string + channel_id?: string + content?: string + webhook_id?: string + author?: DiscordMessageAuthor + edited_timestamp?: string | null +} + +export interface DiscordMessageDeleteEvent { + id?: string + channel_id?: string + webhook_id?: string +} + +interface DiscordReactionEmoji { + id?: string | null + name?: string | null +} + +export interface DiscordMessageReactionAddEvent { + channel_id?: string + message_id?: string + user_id?: string + user?: DiscordMessageAuthor + member?: { + user?: DiscordMessageAuthor + } + emoji?: DiscordReactionEmoji +} + +export interface DiscordMessageReactionRemoveEvent { + channel_id?: string + message_id?: string + user_id?: string + user?: DiscordMessageAuthor + member?: { + user?: DiscordMessageAuthor + } + emoji?: DiscordReactionEmoji +} + +export interface DiscordThreadCreateEvent { + id?: string + parent_id?: string + name?: string + type?: number +} + +const formatDiscordDisplayName = (author?: DiscordMessageAuthor): string => { + if (!author) return "Discord User" + if (author.global_name) return author.global_name + if (author.discriminator && author.discriminator !== "0") { + return `${author.username ?? "discord-user"}#${author.discriminator}` + } + return author.username ?? "Discord User" +} + +const buildAuthorAvatarUrl = (author?: DiscordMessageAuthor): string | null => { + if (!author?.id || !author.avatar) return null + return `https://cdn.discordapp.com/avatars/${author.id}/${author.avatar}.png` +} + +export const normalizeDiscordMessageAttachments = ( + attachments: ReadonlyArray | undefined, +): ReadonlyArray => { + if (!attachments || attachments.length === 0) { + return [] + } + + const normalized: Array = [] + for (const attachment of attachments) { + const fileName = typeof attachment.filename === "string" ? attachment.filename.trim() : "" + const publicUrl = typeof attachment.url === "string" ? attachment.url.trim() : "" + if (!fileName || !publicUrl) { + continue + } + + normalized.push({ + externalAttachmentId: + typeof attachment.id === "string" && attachment.id.trim().length > 0 + ? attachment.id + : undefined, + fileName, + fileSize: + typeof attachment.size === "number" && + Number.isFinite(attachment.size) && + attachment.size >= 0 + ? attachment.size + : 0, + publicUrl, + }) + } + + return normalized +} + +export const extractReactionAuthor = (event: { + member?: { user?: DiscordMessageAuthor } + user?: DiscordMessageAuthor +}) => { + const author = event.member?.user ?? event.user + return { + externalAuthorDisplayName: author ? formatDiscordDisplayName(author) : undefined, + externalAuthorAvatarUrl: author ? buildAuthorAvatarUrl(author) : undefined, + } +} + +const formatDiscordEmoji = (emoji?: DiscordReactionEmoji): string | null => { + if (!emoji?.name) return null + if (emoji.id) return `${emoji.name}:${emoji.id}` + return emoji.name +} + +type ExternalIdDecoder = (value: unknown) => Option.Option + +const decodeExternalChannelId: ExternalIdDecoder = (value) => + Schema.decodeUnknownOption(ExternalChannelId)(value) +const decodeExternalMessageId: ExternalIdDecoder = (value) => + Schema.decodeUnknownOption(ExternalMessageId)(value) +const decodeExternalThreadId: ExternalIdDecoder = (value) => + Schema.decodeUnknownOption(ExternalThreadId)(value) +const decodeExternalUserId: ExternalIdDecoder = (value) => + Schema.decodeUnknownOption(ExternalUserId)(value) +const decodeExternalWebhookId: ExternalIdDecoder = (value) => + Schema.decodeUnknownOption(ExternalWebhookId)(value) + +export const decodeRequiredExternalId = ( + value: unknown, + decode: ExternalIdDecoder, +): Option.Option => decode(value) + +export const decodeOptionalExternalId = ( + value: unknown, + decode: ExternalIdDecoder, +): A | undefined => { + if (value === undefined) return undefined + const decoded = decode(value) + return Option.isSome(decoded) ? decoded.value : undefined +} + +const getValueType = (value: unknown): string => (value === null ? "null" : typeof value) + +type GatewayDirection = "both" | "hazel_to_external" | "external_to_hazel" + +type DiscordGatewayChannelLink = { + readonly syncConnectionId: SyncConnectionId + readonly direction: GatewayDirection +} + +type DiscordGatewayDispatchWorker = Pick< + ServiceMap.Service.Shape, + | "ingestMessageCreate" + | "ingestMessageUpdate" + | "ingestMessageDelete" + | "ingestReactionAdd" + | "ingestReactionRemove" + | "ingestThreadCreate" +> + +const decodeRequiredExternalIdOrWarn = (params: { + eventType: string + field: string + value: unknown + decode: ExternalIdDecoder +}) => + Effect.gen(function* () { + const decoded = decodeRequiredExternalId(params.value, params.decode) + if (Option.isNone(decoded)) { + yield* Effect.logWarning("Discord gateway dropped event: invalid external id", { + eventType: params.eventType, + field: params.field, + valueType: getValueType(params.value), + }) + } + return decoded + }) + +const decodeOptionalExternalIdOrWarn = (params: { + eventType: string + field: string + value: unknown + decode: ExternalIdDecoder +}) => + Effect.gen(function* () { + const decoded = decodeOptionalExternalId(params.value, params.decode) + if (params.value !== undefined && decoded === undefined) { + yield* Effect.logWarning("Discord gateway ignored optional invalid external id", { + eventType: params.eventType, + field: params.field, + valueType: getValueType(params.value), + }) + } + return decoded + }) + +export const createDiscordGatewayDispatchHandlers = (deps: { + discordSyncWorker: DiscordGatewayDispatchWorker + findActiveLinksByExternalChannel: ( + externalChannelId: ExternalChannelId, + ) => Effect.Effect, unknown, never> + isCurrentBotAuthor: (authorId?: string) => Effect.Effect +}) => { + const ingestMessageCreateEvent = Effect.fn("DiscordGatewayService.ingestMessageCreateEvent")(function* ( + event: DiscordMessageCreateEvent, + ) { + if (!event.id || !event.channel_id || typeof event.content !== "string") return + if (event.author?.bot) return + if (yield* deps.isCurrentBotAuthor(event.author?.id)) return + + const externalChannelIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_CREATE", + field: "channel_id", + value: event.channel_id, + decode: decodeExternalChannelId, + }) + if (Option.isNone(externalChannelIdOption)) return + + const externalMessageIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_CREATE", + field: "id", + value: event.id, + decode: decodeExternalMessageId, + }) + if (Option.isNone(externalMessageIdOption)) return + + const externalAuthorId = yield* decodeOptionalExternalIdOrWarn({ + eventType: "MESSAGE_CREATE", + field: "author.id", + value: event.author?.id, + decode: decodeExternalUserId, + }) + + const externalReplyToMessageId = yield* decodeOptionalExternalIdOrWarn({ + eventType: "MESSAGE_CREATE", + field: "message_reference.message_id", + value: event.message_reference?.message_id, + decode: decodeExternalMessageId, + }) + + const externalWebhookId = yield* decodeOptionalExternalIdOrWarn({ + eventType: "MESSAGE_CREATE", + field: "webhook_id", + value: event.webhook_id, + decode: decodeExternalWebhookId, + }) + + const externalAttachments = normalizeDiscordMessageAttachments(event.attachments) + const links = yield* deps.findActiveLinksByExternalChannel(externalChannelIdOption.value) + const inboundLinks = links.filter((link) => link.direction !== "hazel_to_external") + + yield* Effect.forEach(inboundLinks, (link) => + deps.discordSyncWorker.ingestMessageCreate({ + syncConnectionId: link.syncConnectionId, + externalChannelId: externalChannelIdOption.value, + externalMessageId: externalMessageIdOption.value, + externalWebhookId, + content: event.content ?? "", + externalAuthorId, + externalAuthorDisplayName: formatDiscordDisplayName(event.author), + externalAuthorAvatarUrl: buildAuthorAvatarUrl(event.author), + externalReplyToMessageId: externalReplyToMessageId ?? null, + externalAttachments, + dedupeKey: `discord:gateway:create:${externalMessageIdOption.value}`, + }), + ) + }) + + const ingestMessageUpdateEvent = Effect.fn("DiscordGatewayService.ingestMessageUpdateEvent")(function* ( + event: DiscordMessageUpdateEvent, + ) { + if (!event.id || !event.channel_id || typeof event.content !== "string") return + if (event.author?.bot) return + if (yield* deps.isCurrentBotAuthor(event.author?.id)) return + + const externalChannelIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_UPDATE", + field: "channel_id", + value: event.channel_id, + decode: decodeExternalChannelId, + }) + if (Option.isNone(externalChannelIdOption)) return + + const externalMessageIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_UPDATE", + field: "id", + value: event.id, + decode: decodeExternalMessageId, + }) + if (Option.isNone(externalMessageIdOption)) return + + const externalWebhookId = yield* decodeOptionalExternalIdOrWarn({ + eventType: "MESSAGE_UPDATE", + field: "webhook_id", + value: event.webhook_id, + decode: decodeExternalWebhookId, + }) + + const links = yield* deps.findActiveLinksByExternalChannel(externalChannelIdOption.value) + const inboundLinks = links.filter((link) => link.direction !== "hazel_to_external") + const content = event.content + const dedupeSuffix = + event.edited_timestamp ?? + createHash("sha256") + .update(`${externalMessageIdOption.value}:${event.content ?? ""}`) + .digest("hex") + .slice(0, 16) + + yield* Effect.forEach(inboundLinks, (link) => + deps.discordSyncWorker.ingestMessageUpdate({ + syncConnectionId: link.syncConnectionId, + externalChannelId: externalChannelIdOption.value, + externalMessageId: externalMessageIdOption.value, + externalWebhookId, + content, + dedupeKey: `discord:gateway:update:${externalMessageIdOption.value}:${dedupeSuffix}`, + }), + ) + }) + + const ingestMessageDeleteEvent = Effect.fn("DiscordGatewayService.ingestMessageDeleteEvent")(function* ( + event: DiscordMessageDeleteEvent, + ) { + if (!event.id || !event.channel_id) return + + const externalChannelIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_DELETE", + field: "channel_id", + value: event.channel_id, + decode: decodeExternalChannelId, + }) + if (Option.isNone(externalChannelIdOption)) return + + const externalMessageIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_DELETE", + field: "id", + value: event.id, + decode: decodeExternalMessageId, + }) + if (Option.isNone(externalMessageIdOption)) return + + const externalWebhookId = yield* decodeOptionalExternalIdOrWarn({ + eventType: "MESSAGE_DELETE", + field: "webhook_id", + value: event.webhook_id, + decode: decodeExternalWebhookId, + }) + + const links = yield* deps.findActiveLinksByExternalChannel(externalChannelIdOption.value) + const inboundLinks = links.filter((link) => link.direction !== "hazel_to_external") + + yield* Effect.forEach(inboundLinks, (link) => + deps.discordSyncWorker.ingestMessageDelete({ + syncConnectionId: link.syncConnectionId, + externalChannelId: externalChannelIdOption.value, + externalMessageId: externalMessageIdOption.value, + externalWebhookId, + dedupeKey: `discord:gateway:delete:${externalMessageIdOption.value}`, + }), + ) + }) + + const ingestMessageReactionAddEvent = Effect.fn( + "DiscordGatewayService.ingestMessageReactionAddEvent", + )(function* (event: DiscordMessageReactionAddEvent) { + if (!event.channel_id || !event.message_id || !event.user_id) return + + const emoji = formatDiscordEmoji(event.emoji) + if (!emoji) return + + const { externalAuthorDisplayName, externalAuthorAvatarUrl } = extractReactionAuthor(event) + const externalChannelIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_REACTION_ADD", + field: "channel_id", + value: event.channel_id, + decode: decodeExternalChannelId, + }) + if (Option.isNone(externalChannelIdOption)) return + + const externalMessageIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_REACTION_ADD", + field: "message_id", + value: event.message_id, + decode: decodeExternalMessageId, + }) + if (Option.isNone(externalMessageIdOption)) return + + const externalUserIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_REACTION_ADD", + field: "user_id", + value: event.user_id, + decode: decodeExternalUserId, + }) + if (Option.isNone(externalUserIdOption)) return + + const links = yield* deps.findActiveLinksByExternalChannel(externalChannelIdOption.value) + const inboundLinks = links.filter((link) => link.direction !== "hazel_to_external") + + yield* Effect.forEach(inboundLinks, (link) => + deps.discordSyncWorker.ingestReactionAdd({ + syncConnectionId: link.syncConnectionId, + externalChannelId: externalChannelIdOption.value, + externalMessageId: externalMessageIdOption.value, + externalUserId: externalUserIdOption.value, + emoji, + externalAuthorDisplayName, + externalAuthorAvatarUrl, + dedupeKey: `discord:gateway:reaction:add:${externalChannelIdOption.value}:${externalMessageIdOption.value}:${externalUserIdOption.value}:${emoji}`, + }), + ) + }) + + const ingestMessageReactionRemoveEvent = Effect.fn( + "DiscordGatewayService.ingestMessageReactionRemoveEvent", + )(function* (event: DiscordMessageReactionRemoveEvent) { + if (!event.channel_id || !event.message_id || !event.user_id) return + + const emoji = formatDiscordEmoji(event.emoji) + if (!emoji) return + + const { externalAuthorDisplayName, externalAuthorAvatarUrl } = extractReactionAuthor(event) + const externalChannelIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_REACTION_REMOVE", + field: "channel_id", + value: event.channel_id, + decode: decodeExternalChannelId, + }) + if (Option.isNone(externalChannelIdOption)) return + + const externalMessageIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_REACTION_REMOVE", + field: "message_id", + value: event.message_id, + decode: decodeExternalMessageId, + }) + if (Option.isNone(externalMessageIdOption)) return + + const externalUserIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_REACTION_REMOVE", + field: "user_id", + value: event.user_id, + decode: decodeExternalUserId, + }) + if (Option.isNone(externalUserIdOption)) return + + const links = yield* deps.findActiveLinksByExternalChannel(externalChannelIdOption.value) + const inboundLinks = links.filter((link) => link.direction !== "hazel_to_external") + + yield* Effect.forEach(inboundLinks, (link) => + deps.discordSyncWorker.ingestReactionRemove({ + syncConnectionId: link.syncConnectionId, + externalChannelId: externalChannelIdOption.value, + externalMessageId: externalMessageIdOption.value, + externalUserId: externalUserIdOption.value, + emoji, + externalAuthorDisplayName, + externalAuthorAvatarUrl, + dedupeKey: `discord:gateway:reaction:remove:${externalChannelIdOption.value}:${externalMessageIdOption.value}:${externalUserIdOption.value}:${emoji}`, + }), + ) + }) + + const ingestThreadCreateEvent = Effect.fn("DiscordGatewayService.ingestThreadCreateEvent")(function* ( + event: DiscordThreadCreateEvent, + ) { + if (!event.id || !event.parent_id || event.type !== 11) return + + const externalParentChannelIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "THREAD_CREATE", + field: "parent_id", + value: event.parent_id, + decode: decodeExternalChannelId, + }) + if (Option.isNone(externalParentChannelIdOption)) return + + const externalThreadIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "THREAD_CREATE", + field: "id", + value: event.id, + decode: decodeExternalThreadId, + }) + if (Option.isNone(externalThreadIdOption)) return + + const links = yield* deps.findActiveLinksByExternalChannel(externalParentChannelIdOption.value) + const inboundLinks = links.filter((link) => link.direction !== "hazel_to_external") + + yield* Effect.forEach(inboundLinks, (link) => + deps.discordSyncWorker.ingestThreadCreate({ + syncConnectionId: link.syncConnectionId, + externalParentChannelId: externalParentChannelIdOption.value, + externalThreadId: externalThreadIdOption.value, + name: event.name, + dedupeKey: `discord:gateway:thread:create:${externalThreadIdOption.value}`, + }), + ) + }) + + return { + ingestMessageCreateEvent, + ingestMessageUpdateEvent, + ingestMessageDeleteEvent, + ingestMessageReactionAddEvent, + ingestMessageReactionRemoveEvent, + ingestThreadCreateEvent, + } +} diff --git a/apps/backend/src/services/chat-sync/discord-sync-worker.test.ts b/apps/backend/src/services/chat-sync/discord-sync-worker.test.ts index 477fccfaa..f17dad5cb 100644 --- a/apps/backend/src/services/chat-sync/discord-sync-worker.test.ts +++ b/apps/backend/src/services/chat-sync/discord-sync-worker.test.ts @@ -17,6 +17,7 @@ import type { ChannelId, ExternalChannelId, ExternalMessageId, + ExternalThreadId, ExternalUserId, ExternalWebhookId, MessageId, @@ -26,13 +27,14 @@ import type { UserId, } from "@hazel/schema" import { Discord } from "@hazel/integrations" -import { Effect, Layer, Option } from "effect" +import { Effect, Layer, Option, ServiceMap } from "effect" import { ChannelAccessSyncService } from "../channel-access-sync.ts" import { IntegrationBotService } from "../integrations/integration-bot-service.ts" -import { ChatSyncCoreWorker } from "./chat-sync-core-worker.ts" +import { configLayer, serviceShape } from "../../test/effect-helpers.ts" +import { ChatSyncCoreWorker, ChatSyncCoreWorkerMake } from "./chat-sync-core-worker.ts" import { ChatSyncProviderRegistry } from "./chat-sync-provider-registry.ts" import { - DiscordSyncWorker, + DiscordSyncWorker as DiscordSyncWorkerTag, type DiscordIngressMessageCreate, type DiscordIngressMessageUpdate, type DiscordIngressMessageDelete, @@ -40,27 +42,28 @@ import { type DiscordIngressReactionRemove, } from "./discord-sync-worker.ts" -const SYNC_CONNECTION_ID = "00000000-0000-0000-0000-000000000001" as SyncConnectionId -const CHANNEL_LINK_ID = "00000000-0000-0000-0000-000000000002" as SyncChannelLinkId -const HAZEL_CHANNEL_ID = "00000000-0000-0000-0000-000000000003" as ChannelId -const ORGANIZATION_ID = "00000000-0000-0000-0000-000000000004" as OrganizationId -const HAZEL_MESSAGE_ID = "00000000-0000-0000-0000-000000000005" as MessageId -const BOT_USER_ID = "00000000-0000-0000-0000-000000000006" as UserId -const REACTION_USER_ID = "00000000-0000-0000-0000-000000000007" as UserId +const SYNC_CONNECTION_ID = "00000000-0000-4000-8000-000000000001" as SyncConnectionId +const CHANNEL_LINK_ID = "00000000-0000-4000-8000-000000000002" as SyncChannelLinkId +const HAZEL_CHANNEL_ID = "00000000-0000-4000-8000-000000000003" as ChannelId +const ORGANIZATION_ID = "00000000-0000-4000-8000-000000000004" as OrganizationId +const HAZEL_MESSAGE_ID = "00000000-0000-4000-8000-000000000005" as MessageId +const BOT_USER_ID = "00000000-0000-4000-8000-000000000006" as UserId +const REACTION_USER_ID = "00000000-0000-4000-8000-000000000007" as UserId const DISCORD_CHANNEL_ID = "discord-channel-1" as ExternalChannelId const DISCORD_CHANNEL_ID_SNOWFLAKE = "123456789012345678" as ExternalChannelId const DISCORD_MESSAGE_ID = "discord-message-1" as ExternalMessageId const DISCORD_WEBHOOK_ID = "123456789012345678" as ExternalWebhookId const DISCORD_WEBHOOK_TOKEN = "webhook-test-token" const DISCORD_WEBHOOK_MESSAGE_ID = "987654321098765432" as ExternalMessageId +const ATTACHMENT_PUBLIC_URL = "https://cdn.example.com" const DISCORD_USER_ID_1 = "discord-user-1" as ExternalUserId const DISCORD_USER_ID_2 = "discord-user-2" as ExternalUserId const DISCORD_USER_ID_3 = "discord-user-3" as ExternalUserId -const SYNCED_MENTION_USER_ID = "00000000-0000-0000-0000-000000000011" as UserId -const UNSYNCED_MENTION_USER_ID = "00000000-0000-0000-0000-000000000012" as UserId -const INACTIVE_MENTION_USER_ID = "00000000-0000-0000-0000-000000000013" as UserId -const MISSING_EXTERNAL_MENTION_USER_ID = "00000000-0000-0000-0000-000000000014" as UserId -const UNKNOWN_MENTION_USER_ID = "00000000-0000-0000-0000-000000000015" as UserId +const SYNCED_MENTION_USER_ID = "00000000-0000-4000-8000-000000000011" as UserId +const UNSYNCED_MENTION_USER_ID = "00000000-0000-4000-8000-000000000012" as UserId +const INACTIVE_MENTION_USER_ID = "00000000-0000-4000-8000-000000000013" as UserId +const MISSING_EXTERNAL_MENTION_USER_ID = "00000000-0000-4000-8000-000000000014" as UserId +const UNKNOWN_MENTION_USER_ID = "00000000-0000-4000-8000-000000000015" as UserId const DISCORD_WEBHOOK_EMPTY_SETTINGS = { outboundIdentity: { enabled: true, @@ -123,6 +126,8 @@ type WorkerLayerDeps = { databaseExecute?: (query: unknown) => Effect.Effect } +type DiscordSyncWorkerShape = ServiceMap.Service.Shape + const PAYLOAD: DiscordIngressMessageCreate = { syncConnectionId: SYNC_CONNECTION_ID, externalChannelId: DISCORD_CHANNEL_ID, @@ -131,27 +136,61 @@ const PAYLOAD: DiscordIngressMessageCreate = { } const runWorkerEffect = (effect: Effect.Effect) => - Effect.runPromise(effect as Effect.Effect) + Effect.runPromise(Effect.scoped(effect as Effect.Effect)) + +const defaultProviderRegistry = serviceShape({ + getAdapter: () => + Effect.succeed({ + provider: "discord", + createMessage: () => Effect.succeed(DISCORD_MESSAGE_ID), + createMessageWithAttachments: () => Effect.succeed(DISCORD_MESSAGE_ID), + updateMessage: () => Effect.succeed(undefined), + deleteMessage: () => Effect.succeed(undefined), + addReaction: () => Effect.succeed(undefined), + removeReaction: () => Effect.succeed(undefined), + createThread: () => Effect.succeed("123456789012345679" as ExternalThreadId), + }), +}) + +const defaultDiscordApiClient = serviceShape({ + getAccountInfo: () => Effect.succeed({ id: "discord-bot" }), + listGuilds: () => Effect.succeed([]), + listGuildChannels: () => Effect.succeed([]), + createMessage: () => Effect.succeed(DISCORD_MESSAGE_ID), + updateMessage: () => Effect.succeed(undefined), + deleteMessage: () => Effect.succeed(undefined), + createWebhook: () => + Effect.succeed({ + id: DISCORD_WEBHOOK_ID, + token: DISCORD_WEBHOOK_TOKEN, + }), + executeWebhookMessage: () => Effect.succeed(DISCORD_WEBHOOK_MESSAGE_ID), + updateWebhookMessage: () => Effect.succeed(undefined), + deleteWebhookMessage: () => Effect.succeed(undefined), + addReaction: () => Effect.succeed(undefined), + removeReaction: () => Effect.succeed(undefined), + createThread: () => Effect.succeed("123456789012345680"), +}) const makeWorkerLayer = (deps: WorkerLayerDeps) => - DiscordSyncWorker.DefaultWithoutDependencies.pipe( + Layer.effect(DiscordSyncWorkerTag, DiscordSyncWorkerTag.make).pipe( Layer.provide( - ChatSyncCoreWorker.DefaultWithoutDependencies.pipe( + Layer.effect(ChatSyncCoreWorker, ChatSyncCoreWorkerMake).pipe( Layer.provide( deps.providerRegistry ? Layer.succeed( ChatSyncProviderRegistry, - deps.providerRegistry as ChatSyncProviderRegistry, + serviceShape(deps.providerRegistry), ) - : ChatSyncProviderRegistry.layer, + : Layer.succeed(ChatSyncProviderRegistry, defaultProviderRegistry), ), Layer.provide( deps.discordApiClient ? Layer.succeed( Discord.DiscordApiClient, - deps.discordApiClient as Discord.DiscordApiClient, + serviceShape(deps.discordApiClient), ) - : Discord.DiscordApiClient.layer, + : Layer.succeed(Discord.DiscordApiClient, defaultDiscordApiClient), ), ), ), @@ -168,44 +207,69 @@ const makeWorkerLayer = (deps: WorkerLayerDeps) => makeQueryWithSchema: () => Effect.die("not used in this test"), } as any), ), - Layer.provide(Layer.succeed(ChatSyncConnectionRepo, deps.connectionRepo as ChatSyncConnectionRepo)), Layer.provide( - Layer.succeed(ChatSyncChannelLinkRepo, deps.channelLinkRepo as ChatSyncChannelLinkRepo), + Layer.succeed( + ChatSyncConnectionRepo, + serviceShape(deps.connectionRepo), + ), + ), + Layer.provide( + Layer.succeed( + ChatSyncChannelLinkRepo, + serviceShape(deps.channelLinkRepo), + ), ), Layer.provide( - Layer.succeed(ChatSyncMessageLinkRepo, deps.messageLinkRepo as ChatSyncMessageLinkRepo), + Layer.succeed( + ChatSyncMessageLinkRepo, + serviceShape(deps.messageLinkRepo), + ), ), Layer.provide( - Layer.succeed(ChatSyncEventReceiptRepo, deps.eventReceiptRepo as ChatSyncEventReceiptRepo), + Layer.succeed( + ChatSyncEventReceiptRepo, + serviceShape(deps.eventReceiptRepo), + ), ), - Layer.provide(Layer.succeed(MessageRepo, deps.messageRepo as MessageRepo)), + Layer.provide(Layer.succeed(MessageRepo, serviceShape(deps.messageRepo))), Layer.provide( Layer.succeed( MessageOutboxRepo, - (deps.messageOutboxRepo ?? { - insert: () => Effect.succeed([{ id: "outbox-id" } as any]), - }) as MessageOutboxRepo, + serviceShape(deps.messageOutboxRepo ?? { + insert: () => Effect.succeed([{ id: "outbox-id" }]), + }), + ), + ), + Layer.provide( + Layer.succeed( + MessageReactionRepo, + serviceShape(deps.messageReactionRepo), ), ), - Layer.provide(Layer.succeed(MessageReactionRepo, deps.messageReactionRepo as MessageReactionRepo)), - Layer.provide(Layer.succeed(ChannelRepo, deps.channelRepo as ChannelRepo)), + Layer.provide(Layer.succeed(ChannelRepo, serviceShape(deps.channelRepo))), Layer.provide( Layer.succeed( IntegrationConnectionRepo, - deps.integrationConnectionRepo as IntegrationConnectionRepo, + serviceShape(deps.integrationConnectionRepo), ), ), - Layer.provide(Layer.succeed(UserRepo, deps.userRepo as UserRepo)), + Layer.provide(Layer.succeed(UserRepo, serviceShape(deps.userRepo))), Layer.provide( - Layer.succeed(OrganizationMemberRepo, deps.organizationMemberRepo as OrganizationMemberRepo), + Layer.succeed( + OrganizationMemberRepo, + serviceShape(deps.organizationMemberRepo), + ), ), Layer.provide( - Layer.succeed(IntegrationBotService, deps.integrationBotService as IntegrationBotService), + Layer.succeed( + IntegrationBotService, + serviceShape(deps.integrationBotService), + ), ), Layer.provide( Layer.succeed( ChannelAccessSyncService, - deps.channelAccessSyncService as ChannelAccessSyncService, + serviceShape(deps.channelAccessSyncService), ), ), ) @@ -215,6 +279,55 @@ const makeWorkerLayerWithOverrides = (deps: WorkerLayerDeps) => { return layer } +const useWorker = ( + fn: (worker: DiscordSyncWorkerShape) => Effect.Effect, +) => DiscordSyncWorkerTag.use(fn) + +const DiscordSyncWorker = { + ingestMessageCreate: (payload: DiscordIngressMessageCreate) => + useWorker((worker) => worker.ingestMessageCreate(payload)), + ingestMessageUpdate: (payload: DiscordIngressMessageUpdate) => + useWorker((worker) => worker.ingestMessageUpdate(payload)), + ingestMessageDelete: (payload: DiscordIngressMessageDelete) => + useWorker((worker) => worker.ingestMessageDelete(payload)), + ingestReactionAdd: (payload: DiscordIngressReactionAdd) => + useWorker((worker) => worker.ingestReactionAdd(payload)), + ingestReactionRemove: (payload: DiscordIngressReactionRemove) => + useWorker((worker) => worker.ingestReactionRemove(payload)), + syncHazelMessageToDiscord: ( + syncConnectionId: SyncConnectionId, + hazelMessageId: MessageId, + dedupeKeyOverride?: string, + ) => + useWorker((worker) => + worker.syncHazelMessageToDiscord(syncConnectionId, hazelMessageId, dedupeKeyOverride), + ), + syncHazelMessageUpdateToDiscord: ( + syncConnectionId: SyncConnectionId, + hazelMessageId: MessageId, + dedupeKeyOverride?: string, + ) => + useWorker((worker) => + worker.syncHazelMessageUpdateToDiscord( + syncConnectionId, + hazelMessageId, + dedupeKeyOverride, + ), + ), + syncHazelMessageDeleteToDiscord: ( + syncConnectionId: SyncConnectionId, + hazelMessageId: MessageId, + dedupeKeyOverride?: string, + ) => + useWorker((worker) => + worker.syncHazelMessageDeleteToDiscord( + syncConnectionId, + hazelMessageId, + dedupeKeyOverride, + ), + ), +} + describe("DiscordSyncWorker dedupe claim", () => { it("returns deduped and exits before side effects when claim fails", async () => { let connectionLookupCalled = false @@ -390,7 +503,7 @@ describe("DiscordSyncWorker inbound attachment records", () => { } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByExternalMessage: () => Effect.succeed(Option.none()), - insert: () => Effect.succeed([{ id: "message-link-id" } as any]), + insert: () => Effect.succeed([{ id: "message-link-id" }]), } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { claimByDedupeKey: () => Effect.succeed(true), @@ -399,7 +512,7 @@ describe("DiscordSyncWorker inbound attachment records", () => { messageRepo: { insert: (payload: { content: string }) => { insertedContent = payload.content - return Effect.succeed([{ id: HAZEL_MESSAGE_ID, threadChannelId: null } as any]) + return Effect.succeed([{ id: HAZEL_MESSAGE_ID, threadChannelId: null }]) }, } as unknown as MessageRepo, messageReactionRepo: {} as unknown as MessageReactionRepo, @@ -493,7 +606,7 @@ describe("DiscordSyncWorker inbound attachment records", () => { } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByExternalMessage: () => Effect.succeed(Option.none()), - insert: () => Effect.succeed([{ id: "message-link-id" } as any]), + insert: () => Effect.succeed([{ id: "message-link-id" }]), } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { claimByDedupeKey: () => Effect.succeed(true), @@ -502,7 +615,7 @@ describe("DiscordSyncWorker inbound attachment records", () => { messageRepo: { insert: (payload: { content: string }) => { insertedContent = payload.content - return Effect.succeed([{ id: HAZEL_MESSAGE_ID, threadChannelId: null } as any]) + return Effect.succeed([{ id: HAZEL_MESSAGE_ID, threadChannelId: null }]) }, } as unknown as MessageRepo, messageReactionRepo: {} as unknown as MessageReactionRepo, @@ -576,7 +689,7 @@ describe("DiscordSyncWorker inbound attachment records", () => { } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByExternalMessage: () => Effect.succeed(Option.none()), - insert: () => Effect.succeed([{ id: "message-link-id" } as any]), + insert: () => Effect.succeed([{ id: "message-link-id" }]), } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { claimByDedupeKey: () => Effect.succeed(true), @@ -585,7 +698,7 @@ describe("DiscordSyncWorker inbound attachment records", () => { messageRepo: { insert: (payload: { content: string }) => { insertedContent = payload.content - return Effect.succeed([{ id: HAZEL_MESSAGE_ID, threadChannelId: null } as any]) + return Effect.succeed([{ id: HAZEL_MESSAGE_ID, threadChannelId: null }]) }, } as unknown as MessageRepo, messageReactionRepo: {} as unknown as MessageReactionRepo, @@ -712,7 +825,7 @@ describe("DiscordSyncWorker inbound attachment records", () => { messageRepo: { update: (payload: { content?: string }) => { updatedContent = payload.content ?? null - return Effect.succeed([{ id: HAZEL_MESSAGE_ID } as any]) + return Effect.succeed([{ id: HAZEL_MESSAGE_ID }]) }, } as unknown as MessageRepo, messageReactionRepo: {} as unknown as MessageReactionRepo, @@ -780,7 +893,7 @@ describe("DiscordSyncWorker reaction author enrichment", () => { status: "active", }), ), - updateLastSyncedAt: () => Effect.succeed({ id: SYNC_CONNECTION_ID } as any), + updateLastSyncedAt: () => Effect.succeed({ id: SYNC_CONNECTION_ID }), } as unknown as ChatSyncConnectionRepo, channelLinkRepo: { findByExternalChannel: () => @@ -790,7 +903,7 @@ describe("DiscordSyncWorker reaction author enrichment", () => { hazelChannelId: HAZEL_CHANNEL_ID, }), ), - updateLastSyncedAt: () => Effect.succeed({ id: CHANNEL_LINK_ID } as any), + updateLastSyncedAt: () => Effect.succeed({ id: CHANNEL_LINK_ID }), } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByExternalMessage: () => @@ -817,7 +930,7 @@ describe("DiscordSyncWorker reaction author enrichment", () => { id: REACTION_USER_ID, messageId: HAZEL_MESSAGE_ID, channelId: HAZEL_CHANNEL_ID, - userId: "00000000-0000-0000-0000-000000000008", + userId: "00000000-0000-4000-8000-000000000008", emoji: "🚀", }, ]), @@ -892,7 +1005,7 @@ describe("DiscordSyncWorker reaction author enrichment", () => { status: "active", }), ), - updateLastSyncedAt: () => Effect.succeed({ id: SYNC_CONNECTION_ID } as any), + updateLastSyncedAt: () => Effect.succeed({ id: SYNC_CONNECTION_ID }), } as unknown as ChatSyncConnectionRepo, channelLinkRepo: { findByExternalChannel: () => @@ -902,7 +1015,7 @@ describe("DiscordSyncWorker reaction author enrichment", () => { hazelChannelId: HAZEL_CHANNEL_ID, }), ), - updateLastSyncedAt: () => Effect.succeed({ id: CHANNEL_LINK_ID } as any), + updateLastSyncedAt: () => Effect.succeed({ id: CHANNEL_LINK_ID }), } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByExternalMessage: () => @@ -929,7 +1042,7 @@ describe("DiscordSyncWorker reaction author enrichment", () => { id: REACTION_USER_ID, messageId: HAZEL_MESSAGE_ID, channelId: HAZEL_CHANNEL_ID, - userId: "00000000-0000-0000-0000-000000000008", + userId: "00000000-0000-4000-8000-000000000008", emoji: "🚀", }, ]), @@ -1000,7 +1113,7 @@ describe("DiscordSyncWorker reaction author enrichment", () => { status: "active", }), ), - updateLastSyncedAt: () => Effect.succeed({ id: SYNC_CONNECTION_ID } as any), + updateLastSyncedAt: () => Effect.succeed({ id: SYNC_CONNECTION_ID }), } as unknown as ChatSyncConnectionRepo, channelLinkRepo: { findByExternalChannel: () => @@ -1010,7 +1123,7 @@ describe("DiscordSyncWorker reaction author enrichment", () => { hazelChannelId: HAZEL_CHANNEL_ID, }), ), - updateLastSyncedAt: () => Effect.succeed({ id: CHANNEL_LINK_ID } as any), + updateLastSyncedAt: () => Effect.succeed({ id: CHANNEL_LINK_ID }), } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByExternalMessage: () => @@ -1033,7 +1146,7 @@ describe("DiscordSyncWorker reaction author enrichment", () => { findByMessageUserEmoji: () => Effect.succeed( Option.some({ - id: "00000000-0000-0000-0000-000000000008", + id: "00000000-0000-4000-8000-000000000008", messageId: HAZEL_MESSAGE_ID, }), ), @@ -1329,7 +1442,7 @@ describe("DiscordSyncWorker outbound webhook dispatch", () => { }) => { insertedExternalMessageId = payload.externalMessageId return Effect.succeed([ - { id: "message-link-id", channelLinkId: payload.channelLinkId } as any, + { id: "message-link-id", channelLinkId: payload.channelLinkId }, ]) }, } as unknown as ChatSyncMessageLinkRepo, @@ -1423,7 +1536,7 @@ describe("DiscordSyncWorker outbound webhook dispatch", () => { } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByHazelMessage: () => Effect.succeed(Option.none()), - insert: () => Effect.succeed([{ id: "message-link-id" } as any]), + insert: () => Effect.succeed([{ id: "message-link-id" }]), } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { claimByDedupeKey: () => Effect.succeed(true), @@ -1585,7 +1698,7 @@ describe("DiscordSyncWorker outbound webhook dispatch", () => { } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByHazelMessage: () => Effect.succeed(Option.none()), - insert: () => Effect.succeed([{ id: "message-link-id" } as any]), + insert: () => Effect.succeed([{ id: "message-link-id" }]), } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { claimByDedupeKey: () => Effect.succeed(true), @@ -2107,7 +2220,7 @@ describe("DiscordSyncWorker outbound webhook dispatch", () => { externalMessageId: DISCORD_WEBHOOK_MESSAGE_ID, }), ), - softDelete: () => Effect.succeed([{ id: "message-link-id" } as any]), + softDelete: () => Effect.succeed([{ id: "message-link-id" }]), updateLastSyncedAt: () => Effect.succeed([]), } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { @@ -2232,12 +2345,12 @@ describe("DiscordSyncWorker outbound webhook dispatch", () => { updateSettings: (id: SyncChannelLinkId, settings: Record | null) => { persistedLinkId = id updatedSettings = settings - return Effect.succeed([{ id }] as any) + return Effect.succeed([{ id }]) }, } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByHazelMessage: () => Effect.succeed(Option.none()), - insert: () => Effect.succeed([{ id: "message-link-id" } as any]), + insert: () => Effect.succeed([{ id: "message-link-id" }]), } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { claimByDedupeKey: () => Effect.succeed(true), @@ -2370,12 +2483,12 @@ describe("DiscordSyncWorker outbound webhook dispatch", () => { ), updateLastSyncedAt: () => Effect.succeed([]), updateSettings: () => { - return Effect.succeed([{ id: CHANNEL_LINK_ID }] as any) + return Effect.succeed([{ id: CHANNEL_LINK_ID }]) }, } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByHazelMessage: () => Effect.succeed(Option.none()), - insert: () => Effect.succeed([{ id: "message-link-id" } as any]), + insert: () => Effect.succeed([{ id: "message-link-id" }]), } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { claimByDedupeKey: () => Effect.succeed(true), @@ -2497,12 +2610,12 @@ describe("DiscordSyncWorker outbound webhook dispatch", () => { updateLastSyncedAt: () => Effect.succeed([]), updateSettings: (id: SyncChannelLinkId, settings: Record | null) => { updatedSettings = settings - return Effect.succeed([{ id }] as any) + return Effect.succeed([{ id }]) }, } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByHazelMessage: () => Effect.succeed(Option.none()), - insert: () => Effect.succeed([{ id: "message-link-id" } as any]), + insert: () => Effect.succeed([{ id: "message-link-id" }]), } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { claimByDedupeKey: () => Effect.succeed(true), @@ -2646,12 +2759,12 @@ describe("DiscordSyncWorker outbound webhook dispatch", () => { updateLastSyncedAt: () => Effect.succeed([]), updateSettings: (id: SyncChannelLinkId, settings: Record | null) => { updatedSettings = settings - return Effect.succeed([{ id }] as any) + return Effect.succeed([{ id }]) }, } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByHazelMessage: () => Effect.succeed(Option.none()), - insert: () => Effect.succeed([{ id: "message-link-id" } as any]), + insert: () => Effect.succeed([{ id: "message-link-id" }]), } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { claimByDedupeKey: () => Effect.succeed(true), @@ -2851,33 +2964,24 @@ describe("DiscordSyncWorker outbound attachments primitive", () => { databaseExecute: () => Effect.succeed([ { - id: "00000000-0000-0000-0000-000000000101", + id: "00000000-0000-4000-8000-000000000101", fileName: "diagram.png", fileSize: 2048, }, ]), }) - const originalS3PublicUrl = process.env.S3_PUBLIC_URL - process.env.S3_PUBLIC_URL = "https://cdn.example.com" - try { - const result = await runWorkerEffect( - DiscordSyncWorker.syncHazelMessageToDiscord(SYNC_CONNECTION_ID, HAZEL_MESSAGE_ID).pipe( - Effect.provide(layer), - ), - ) + const result = await runWorkerEffect( + DiscordSyncWorker.syncHazelMessageToDiscord(SYNC_CONNECTION_ID, HAZEL_MESSAGE_ID).pipe( + Effect.provide(layer), + Effect.provide(configLayer({ S3_PUBLIC_URL: ATTACHMENT_PUBLIC_URL })), + ), + ) - expect(result.status).toBe("synced") - expect(createMessageWithAttachmentsCalled).toBe(true) - expect(createMessageCalled).toBe(false) - expect(receivedAttachmentCount).toBe(1) - } finally { - if (originalS3PublicUrl === undefined) { - delete process.env.S3_PUBLIC_URL - } else { - process.env.S3_PUBLIC_URL = originalS3PublicUrl - } - } + expect(result.status).toBe("synced") + expect(createMessageWithAttachmentsCalled).toBe(true) + expect(createMessageCalled).toBe(false) + expect(receivedAttachmentCount).toBe(1) }) it("keeps using createMessage when no attachments are present", async () => { @@ -2912,7 +3016,7 @@ describe("DiscordSyncWorker outbound attachments primitive", () => { messageLinkRepo: { findByHazelMessage: () => Effect.succeed(Option.none()), insert: () => - Effect.succeed([{ id: "message-link-id", channelLinkId: CHANNEL_LINK_ID } as any]), + Effect.succeed([{ id: "message-link-id", channelLinkId: CHANNEL_LINK_ID }]), } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { claimByDedupeKey: () => Effect.succeed(true), @@ -3079,32 +3183,25 @@ describe("DiscordSyncWorker outbound attachments primitive", () => { databaseExecute: () => Effect.succeed([ { - id: "00000000-0000-0000-0000-000000000201", + id: "00000000-0000-4000-8000-000000000201", fileName: "capture.jpg", fileSize: 5120, }, ]), }) - const originalS3PublicUrl = process.env.S3_PUBLIC_URL - process.env.S3_PUBLIC_URL = "https://cdn.example.com" - try { - const result = await runWorkerEffect( - DiscordSyncWorker.syncHazelMessageToDiscord(SYNC_CONNECTION_ID, HAZEL_MESSAGE_ID).pipe( - Effect.provide(layer), - ), - ) + const result = await runWorkerEffect( + DiscordSyncWorker.syncHazelMessageToDiscord(SYNC_CONNECTION_ID, HAZEL_MESSAGE_ID).pipe( + Effect.provide(layer), + Effect.provide(configLayer({ S3_PUBLIC_URL: ATTACHMENT_PUBLIC_URL })), + ), + ) - expect(result.status).toBe("synced") - expect(createMessageWithAttachmentsCalled).toBe(true) - expect(firstAttachmentUrl).toBe("https://cdn.example.com/00000000-0000-0000-0000-000000000201") - } finally { - if (originalS3PublicUrl === undefined) { - delete process.env.S3_PUBLIC_URL - } else { - process.env.S3_PUBLIC_URL = originalS3PublicUrl - } - } + expect(result.status).toBe("synced") + expect(createMessageWithAttachmentsCalled).toBe(true) + expect(firstAttachmentUrl).toBe( + `${ATTACHMENT_PUBLIC_URL}/00000000-0000-4000-8000-000000000201`, + ) }) it("fails with configuration error when attachments exist and S3_PUBLIC_URL is missing", async () => { @@ -3176,7 +3273,7 @@ describe("DiscordSyncWorker outbound attachments primitive", () => { databaseExecute: () => Effect.succeed([ { - id: "00000000-0000-0000-0000-000000000301", + id: "00000000-0000-4000-8000-000000000301", fileName: "missing-env.txt", fileSize: 10, }, diff --git a/apps/backend/src/services/chat-sync/discord-sync-worker.ts b/apps/backend/src/services/chat-sync/discord-sync-worker.ts index 32a44c761..f5599d404 100644 --- a/apps/backend/src/services/chat-sync/discord-sync-worker.ts +++ b/apps/backend/src/services/chat-sync/discord-sync-worker.ts @@ -32,9 +32,6 @@ export { DiscordSyncMessageNotFoundError, } -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- tag-only service; real make provided via DiscordSyncWorkerMake / DiscordSyncWorkerLayer -export class DiscordSyncWorker extends ServiceMap.Service()("DiscordSyncWorker") {} - const DiscordSyncWorkerMake = Effect.gen(function* () { const coreWorker = yield* ChatSyncCoreWorker @@ -224,6 +221,10 @@ const DiscordSyncWorkerMake = Effect.gen(function* () { } }) -export const DiscordSyncWorkerLayer = Layer.effect(DiscordSyncWorker, DiscordSyncWorkerMake).pipe( +export class DiscordSyncWorker extends ServiceMap.Service()("DiscordSyncWorker", { + make: DiscordSyncWorkerMake, +}) {} + +export const DiscordSyncWorkerLayer = Layer.effect(DiscordSyncWorker, DiscordSyncWorker.make).pipe( Layer.provide(ChatSyncCoreWorkerLayer), ) diff --git a/apps/backend/src/services/connect-conversation-service.test.ts b/apps/backend/src/services/connect-conversation-service.test.ts index 844f0f4bf..a07f46d8e 100644 --- a/apps/backend/src/services/connect-conversation-service.test.ts +++ b/apps/backend/src/services/connect-conversation-service.test.ts @@ -16,20 +16,21 @@ import type { OrganizationId, UserId, } from "@hazel/schema" -import { Effect, Layer, Option } from "effect" +import { Effect, Layer, Option, ServiceMap } from "effect" +import { serviceShape } from "../test/effect-helpers" import { ChannelAccessSyncService } from "./channel-access-sync" import { ConnectConversationService } from "./connect-conversation-service" import { OrgResolver } from "./org-resolver" -const HOST_ORG_ID = "00000000-0000-0000-0000-000000000101" as OrganizationId -const GUEST_ORG_ID = "00000000-0000-0000-0000-000000000102" as OrganizationId -const OTHER_GUEST_ORG_ID = "00000000-0000-0000-0000-000000000103" as OrganizationId -const CONVERSATION_ID = "00000000-0000-0000-0000-000000000201" as ConnectConversationId -const HOST_CHANNEL_ID = "00000000-0000-0000-0000-000000000301" as ChannelId -const GUEST_CHANNEL_ID = "00000000-0000-0000-0000-000000000302" as ChannelId -const OTHER_GUEST_CHANNEL_ID = "00000000-0000-0000-0000-000000000303" as ChannelId -const HOST_USER_ID = "00000000-0000-0000-0000-000000000401" as UserId -const GUEST_USER_ID = "00000000-0000-0000-0000-000000000402" as UserId +const HOST_ORG_ID = "00000000-0000-4000-8000-000000000101" as OrganizationId +const GUEST_ORG_ID = "00000000-0000-4000-8000-000000000102" as OrganizationId +const OTHER_GUEST_ORG_ID = "00000000-0000-4000-8000-000000000103" as OrganizationId +const CONVERSATION_ID = "00000000-0000-4000-8000-000000000201" as ConnectConversationId +const HOST_CHANNEL_ID = "00000000-0000-4000-8000-000000000301" as ChannelId +const GUEST_CHANNEL_ID = "00000000-0000-4000-8000-000000000302" as ChannelId +const OTHER_GUEST_CHANNEL_ID = "00000000-0000-4000-8000-000000000303" as ChannelId +const HOST_USER_ID = "00000000-0000-4000-8000-000000000401" as UserId +const GUEST_USER_ID = "00000000-0000-4000-8000-000000000402" as UserId type MutableConversation = { id: ConnectConversationId @@ -69,28 +70,33 @@ type MutableParticipant = { deletedAt: Date | null } +type ConnectConversationServiceShape = ServiceMap.Service.Shape +type ConnectParticipantUpsertInput = Parameters< + ServiceMap.Service.Shape["upsertByChannelAndUser"] +>[0] + const makeChannelRepoLayer = () => - Layer.succeed(ChannelRepo, { + Layer.succeed(ChannelRepo, serviceShape({ findById: () => Effect.succeed(Option.none()), - } as unknown as ChannelRepo) + })) const makeMessageRepoLayer = () => - Layer.succeed(MessageRepo, { + Layer.succeed(MessageRepo, serviceShape({ backfillConversationIdForChannel: () => Effect.succeed(undefined), - } as unknown as MessageRepo) + })) const makeMessageReactionRepoLayer = () => - Layer.succeed(MessageReactionRepo, { + Layer.succeed(MessageReactionRepo, serviceShape({ backfillConversationIdForChannel: () => Effect.succeed(undefined), - } as unknown as MessageReactionRepo) + })) const makeOrgResolverLayer = () => - Layer.succeed(OrgResolver, { + Layer.succeed(OrgResolver, serviceShape({ fromChannelWithAccess: () => Effect.succeed(undefined), - } as unknown as OrgResolver) + })) const makeConversationRepoLayer = (conversation: MutableConversation) => - Layer.succeed(ConnectConversationRepo, { + Layer.succeed(ConnectConversationRepo, serviceShape({ findById: (id: ConnectConversationId) => Effect.succeed(id === conversation.id ? Option.some(conversation) : Option.none()), update: (patch: Partial & { id: ConnectConversationId }) => @@ -99,10 +105,10 @@ const makeConversationRepoLayer = (conversation: MutableConversation) => return conversation }), insert: () => Effect.die("not implemented"), - } as unknown as ConnectConversationRepo) + })) const makeMountRepoLayer = (mounts: MutableMount[]) => - Layer.succeed(ConnectConversationChannelRepo, { + Layer.succeed(ConnectConversationChannelRepo, serviceShape({ findByChannelId: (channelId: ChannelId) => Effect.succeed( Option.fromNullishOr( @@ -121,10 +127,10 @@ const makeMountRepoLayer = (mounts: MutableMount[]) => return mount }), insert: () => Effect.die("not implemented"), - } as unknown as ConnectConversationChannelRepo) + })) const makeParticipantRepoLayer = (participants: MutableParticipant[]) => - Layer.succeed(ConnectParticipantRepo, { + Layer.succeed(ConnectParticipantRepo, serviceShape({ listByConversation: (conversationId: ConnectConversationId) => Effect.succeed( participants.filter( @@ -142,15 +148,15 @@ const makeParticipantRepoLayer = (participants: MutableParticipant[]) => findByChannelAndUser: () => Effect.succeed(Option.none()), insert: () => Effect.die("not implemented"), upsertByChannelAndUser: () => Effect.die("not implemented"), - } as unknown as ConnectParticipantRepo) + })) const makeChannelAccessSyncLayer = (syncedChannels: ChannelId[]) => - Layer.succeed(ChannelAccessSyncService, { + Layer.succeed(ChannelAccessSyncService, serviceShape({ syncChannel: (channelId: ChannelId) => Effect.sync(() => { syncedChannels.push(channelId) }), - } as unknown as ChannelAccessSyncService) + })) const makeServiceLayer = (params: { conversation: MutableConversation @@ -158,7 +164,7 @@ const makeServiceLayer = (params: { participants: MutableParticipant[] syncedChannels: ChannelId[] }) => - ConnectConversationService.DefaultWithoutDependencies.pipe( + Layer.effect(ConnectConversationService, ConnectConversationService.make).pipe( Layer.provide(makeChannelRepoLayer()), Layer.provide(makeConversationRepoLayer(params.conversation)), Layer.provide(makeMountRepoLayer(params.mounts)), @@ -169,11 +175,9 @@ const makeServiceLayer = (params: { Layer.provide(makeOrgResolverLayer()), ) -const useService = (fn: (service: ConnectConversationService) => Effect.Effect) => - Effect.gen(function* () { - const service = yield* ConnectConversationService - return yield* fn(service) - }) +const useService = ( + fn: (service: ConnectConversationServiceShape) => Effect.Effect, +) => ConnectConversationService.use(fn) describe("ConnectConversationService", () => { it("returns the existing mount when conversation creation races on unique constraints", async () => { @@ -190,7 +194,7 @@ describe("ConnectConversationService", () => { deletedAt: null, } const existingMount: MutableMount = { - id: "00000000-0000-0000-0000-000000000411" as ConnectConversationChannelId, + id: "00000000-0000-4000-8000-000000000411" as ConnectConversationChannelId, conversationId: CONVERSATION_ID, organizationId: HOST_ORG_ID, channelId: HOST_CHANNEL_ID, @@ -204,9 +208,9 @@ describe("ConnectConversationService", () => { const backfills: Array<{ kind: "message" | "reaction"; conversationId: ConnectConversationId }> = [] let findByChannelCalls = 0 - const layer = ConnectConversationService.DefaultWithoutDependencies.pipe( + const layer = Layer.effect(ConnectConversationService, ConnectConversationService.make).pipe( Layer.provide( - Layer.succeed(ChannelRepo, { + Layer.succeed(ChannelRepo, serviceShape({ findById: () => Effect.succeed( Option.some({ @@ -214,10 +218,10 @@ describe("ConnectConversationService", () => { organizationId: HOST_ORG_ID, }), ), - } as unknown as ChannelRepo), + })), ), Layer.provide( - Layer.succeed(ConnectConversationRepo, { + Layer.succeed(ConnectConversationRepo, serviceShape({ insert: () => Effect.fail( new Database.DatabaseError({ @@ -226,10 +230,12 @@ describe("ConnectConversationService", () => { }), ), findByHostChannel: () => Effect.succeed(Option.some(existingConversation)), - } as unknown as ConnectConversationRepo), + })), ), Layer.provide( - Layer.succeed(ConnectConversationChannelRepo, { + Layer.succeed( + ConnectConversationChannelRepo, + serviceShape({ findByChannelId: () => Effect.sync(() => { findByChannelCalls += 1 @@ -243,11 +249,12 @@ describe("ConnectConversationService", () => { }), ), findByConversationId: () => Effect.succeed([existingMount]), - } as unknown as ConnectConversationChannelRepo), + }), + ), ), Layer.provide(makeParticipantRepoLayer([])), Layer.provide( - Layer.succeed(MessageRepo, { + Layer.succeed(MessageRepo, serviceShape({ backfillConversationIdForChannel: ( _channelId: ChannelId, conversationId: ConnectConversationId, @@ -255,10 +262,10 @@ describe("ConnectConversationService", () => { Effect.sync(() => { backfills.push({ kind: "message", conversationId }) }), - } as unknown as MessageRepo), + })), ), Layer.provide( - Layer.succeed(MessageReactionRepo, { + Layer.succeed(MessageReactionRepo, serviceShape({ backfillConversationIdForChannel: ( _channelId: ChannelId, conversationId: ConnectConversationId, @@ -266,7 +273,7 @@ describe("ConnectConversationService", () => { Effect.sync(() => { backfills.push({ kind: "reaction", conversationId }) }), - } as unknown as MessageReactionRepo), + })), ), Layer.provide(makeChannelAccessSyncLayer([])), Layer.provide(makeOrgResolverLayer()), @@ -290,9 +297,9 @@ describe("ConnectConversationService", () => { const now = new Date("2026-03-13T12:00:00.000Z") const transactionChecks: boolean[] = [] - const layer = ConnectConversationService.DefaultWithoutDependencies.pipe( + const layer = Layer.effect(ConnectConversationService, ConnectConversationService.make).pipe( Layer.provide( - Layer.succeed(ChannelRepo, { + Layer.succeed(ChannelRepo, serviceShape({ findById: () => Effect.succeed( Option.some({ @@ -300,10 +307,10 @@ describe("ConnectConversationService", () => { organizationId: HOST_ORG_ID, }), ), - } as unknown as ChannelRepo), + })), ), Layer.provide( - Layer.succeed(ConnectConversationRepo, { + Layer.succeed(ConnectConversationRepo, serviceShape({ insert: () => Effect.succeed([ { @@ -318,15 +325,17 @@ describe("ConnectConversationService", () => { deletedAt: null, }, ]), - } as unknown as ConnectConversationRepo), + })), ), Layer.provide( - Layer.succeed(ConnectConversationChannelRepo, { + Layer.succeed( + ConnectConversationChannelRepo, + serviceShape({ findByChannelId: () => Effect.succeed(Option.none()), insert: () => Effect.succeed([ { - id: "00000000-0000-0000-0000-000000000412" as ConnectConversationChannelId, + id: "00000000-0000-4000-8000-000000000412" as ConnectConversationChannelId, conversationId: CONVERSATION_ID, organizationId: HOST_ORG_ID, channelId: HOST_CHANNEL_ID, @@ -338,11 +347,12 @@ describe("ConnectConversationService", () => { deletedAt: null, }, ]), - } as unknown as ConnectConversationChannelRepo), + }), + ), ), Layer.provide(makeParticipantRepoLayer([])), Layer.provide( - Layer.succeed(MessageRepo, { + Layer.succeed(MessageRepo, serviceShape({ backfillConversationIdForChannel: () => Effect.serviceOption(Database.TransactionContext).pipe( Effect.tap((maybeTx) => @@ -352,10 +362,10 @@ describe("ConnectConversationService", () => { ), Effect.as(undefined), ), - } as unknown as MessageRepo), + })), ), Layer.provide( - Layer.succeed(MessageReactionRepo, { + Layer.succeed(MessageReactionRepo, serviceShape({ backfillConversationIdForChannel: () => Effect.serviceOption(Database.TransactionContext).pipe( Effect.tap((maybeTx) => @@ -365,7 +375,7 @@ describe("ConnectConversationService", () => { ), Effect.as(undefined), ), - } as unknown as MessageReactionRepo), + })), ), Layer.provide(makeChannelAccessSyncLayer([])), Layer.provide(makeOrgResolverLayer()), @@ -395,7 +405,7 @@ describe("ConnectConversationService", () => { let mountFetchCount = 0 const mounts: MutableMount[] = [ { - id: "00000000-0000-0000-0000-000000000921" as ConnectConversationChannelId, + id: "00000000-0000-4000-8000-000000000921" as ConnectConversationChannelId, conversationId: CONVERSATION_ID, organizationId: HOST_ORG_ID, channelId: HOST_CHANNEL_ID, @@ -407,7 +417,7 @@ describe("ConnectConversationService", () => { deletedAt: null, }, { - id: "00000000-0000-0000-0000-000000000922" as ConnectConversationChannelId, + id: "00000000-0000-4000-8000-000000000922" as ConnectConversationChannelId, conversationId: CONVERSATION_ID, organizationId: GUEST_ORG_ID, channelId: GUEST_CHANNEL_ID, @@ -420,26 +430,37 @@ describe("ConnectConversationService", () => { }, ] - const layer = ConnectConversationService.DefaultWithoutDependencies.pipe( + const layer = Layer.effect(ConnectConversationService, ConnectConversationService.make).pipe( Layer.provide(makeChannelRepoLayer()), - Layer.provide(Layer.succeed(ConnectConversationRepo, {} as unknown as ConnectConversationRepo)), Layer.provide( - Layer.succeed(ConnectConversationChannelRepo, { + Layer.succeed( + ConnectConversationRepo, + serviceShape({}), + ), + ), + Layer.provide( + Layer.succeed( + ConnectConversationChannelRepo, + serviceShape({ findByConversationId: () => Effect.sync(() => { mountFetchCount += 1 return mounts }), - } as unknown as ConnectConversationChannelRepo), + }), + ), ), Layer.provide( - Layer.succeed(ConnectParticipantRepo, { - upsertByChannelAndUser: (row: any) => + Layer.succeed( + ConnectParticipantRepo, + serviceShape({ + upsertByChannelAndUser: (row: ConnectParticipantUpsertInput) => Effect.sync(() => { upserts.push(row) return row }), - } as unknown as ConnectParticipantRepo), + }), + ), ), Layer.provide(makeMessageRepoLayer()), Layer.provide(makeMessageReactionRepoLayer()), @@ -508,7 +529,7 @@ describe("ConnectConversationService", () => { } const mounts: MutableMount[] = [ { - id: "00000000-0000-0000-0000-000000000501" as ConnectConversationChannelId, + id: "00000000-0000-4000-8000-000000000501" as ConnectConversationChannelId, conversationId: CONVERSATION_ID, organizationId: HOST_ORG_ID, channelId: HOST_CHANNEL_ID, @@ -520,7 +541,7 @@ describe("ConnectConversationService", () => { deletedAt: null, }, { - id: "00000000-0000-0000-0000-000000000502" as ConnectConversationChannelId, + id: "00000000-0000-4000-8000-000000000502" as ConnectConversationChannelId, conversationId: CONVERSATION_ID, organizationId: GUEST_ORG_ID, channelId: GUEST_CHANNEL_ID, @@ -534,7 +555,7 @@ describe("ConnectConversationService", () => { ] const participants: MutableParticipant[] = [ { - id: "00000000-0000-0000-0000-000000000601" as ConnectParticipantId, + id: "00000000-0000-4000-8000-000000000601" as ConnectParticipantId, conversationId: CONVERSATION_ID, channelId: HOST_CHANNEL_ID, userId: GUEST_USER_ID, @@ -546,7 +567,7 @@ describe("ConnectConversationService", () => { deletedAt: null, }, { - id: "00000000-0000-0000-0000-000000000602" as ConnectParticipantId, + id: "00000000-0000-4000-8000-000000000602" as ConnectParticipantId, conversationId: CONVERSATION_ID, channelId: GUEST_CHANNEL_ID, userId: GUEST_USER_ID, @@ -588,7 +609,7 @@ describe("ConnectConversationService", () => { } const mounts: MutableMount[] = [ { - id: "00000000-0000-0000-0000-000000000701" as ConnectConversationChannelId, + id: "00000000-0000-4000-8000-000000000701" as ConnectConversationChannelId, conversationId: CONVERSATION_ID, organizationId: HOST_ORG_ID, channelId: HOST_CHANNEL_ID, @@ -600,7 +621,7 @@ describe("ConnectConversationService", () => { deletedAt: null, }, { - id: "00000000-0000-0000-0000-000000000702" as ConnectConversationChannelId, + id: "00000000-0000-4000-8000-000000000702" as ConnectConversationChannelId, conversationId: CONVERSATION_ID, organizationId: GUEST_ORG_ID, channelId: GUEST_CHANNEL_ID, @@ -612,7 +633,7 @@ describe("ConnectConversationService", () => { deletedAt: null, }, { - id: "00000000-0000-0000-0000-000000000703" as ConnectConversationChannelId, + id: "00000000-0000-4000-8000-000000000703" as ConnectConversationChannelId, conversationId: CONVERSATION_ID, organizationId: OTHER_GUEST_ORG_ID, channelId: OTHER_GUEST_CHANNEL_ID, @@ -626,7 +647,7 @@ describe("ConnectConversationService", () => { ] const participants: MutableParticipant[] = [ { - id: "00000000-0000-0000-0000-000000000801" as ConnectParticipantId, + id: "00000000-0000-4000-8000-000000000801" as ConnectParticipantId, conversationId: CONVERSATION_ID, channelId: HOST_CHANNEL_ID, userId: GUEST_USER_ID, @@ -638,7 +659,7 @@ describe("ConnectConversationService", () => { deletedAt: null, }, { - id: "00000000-0000-0000-0000-000000000802" as ConnectParticipantId, + id: "00000000-0000-4000-8000-000000000802" as ConnectParticipantId, conversationId: CONVERSATION_ID, channelId: OTHER_GUEST_CHANNEL_ID, userId: HOST_USER_ID, @@ -684,7 +705,7 @@ describe("ConnectConversationService", () => { } const mounts: MutableMount[] = [ { - id: "00000000-0000-0000-0000-000000000901" as ConnectConversationChannelId, + id: "00000000-0000-4000-8000-000000000901" as ConnectConversationChannelId, conversationId: CONVERSATION_ID, organizationId: HOST_ORG_ID, channelId: HOST_CHANNEL_ID, @@ -696,7 +717,7 @@ describe("ConnectConversationService", () => { deletedAt: null, }, { - id: "00000000-0000-0000-0000-000000000902" as ConnectConversationChannelId, + id: "00000000-0000-4000-8000-000000000902" as ConnectConversationChannelId, conversationId: CONVERSATION_ID, organizationId: GUEST_ORG_ID, channelId: GUEST_CHANNEL_ID, @@ -710,7 +731,7 @@ describe("ConnectConversationService", () => { ] const participants: MutableParticipant[] = [ { - id: "00000000-0000-0000-0000-000000001001" as ConnectParticipantId, + id: "00000000-0000-4000-8000-000000001001" as ConnectParticipantId, conversationId: CONVERSATION_ID, channelId: HOST_CHANNEL_ID, userId: HOST_USER_ID, @@ -722,7 +743,7 @@ describe("ConnectConversationService", () => { deletedAt: null, }, { - id: "00000000-0000-0000-0000-000000001002" as ConnectParticipantId, + id: "00000000-0000-4000-8000-000000001002" as ConnectParticipantId, conversationId: CONVERSATION_ID, channelId: GUEST_CHANNEL_ID, userId: GUEST_USER_ID, diff --git a/apps/backend/src/services/message-outbox-dispatcher.test.ts b/apps/backend/src/services/message-outbox-dispatcher.test.ts index 97ea047c6..6ae35a1d4 100644 --- a/apps/backend/src/services/message-outbox-dispatcher.test.ts +++ b/apps/backend/src/services/message-outbox-dispatcher.test.ts @@ -13,13 +13,13 @@ import { Effect, Layer, Redacted, ServiceMap } from "effect" import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import { EnvVars } from "../lib/env-vars" import { createChatSyncDbHarness, type ChatSyncDbHarness } from "../test/chat-sync-db-harness" -import { buildServiceLayer } from "../test/effect-helpers" +import { serviceShape } from "../test/effect-helpers" import { MessageOutboxDispatcher } from "./message-outbox-dispatcher" import { MessageSideEffectService } from "./message-side-effect-service" -const CHANNEL_ID = "00000000-0000-0000-0000-000000000001" as ChannelId -const MESSAGE_ID = "00000000-0000-0000-0000-000000000002" as MessageId -const AUTHOR_ID = "00000000-0000-0000-0000-000000000003" as UserId +const CHANNEL_ID = "00000000-0000-4000-8000-000000000001" as ChannelId +const MESSAGE_ID = "00000000-0000-4000-8000-000000000002" as MessageId +const AUTHOR_ID = "00000000-0000-4000-8000-000000000003" as UserId type SideEffectCall = | { eventType: "message_created"; payload: MessageCreatedPayload; dedupeKey: string } @@ -44,14 +44,14 @@ const runDispatcherEffect = ( Effect.runPromise( Effect.scoped( make.pipe( - Effect.provide(buildServiceLayer(MessageOutboxDispatcher)), + Effect.provide(Layer.effect(MessageOutboxDispatcher, MessageOutboxDispatcher.make)), Effect.provide(Layer.succeed(MessageSideEffectService, sideEffects)), Effect.provide(MessageOutboxRepo.layer), Effect.provide( - Layer.succeed(EnvVars, { + Layer.succeed(EnvVars, serviceShape({ IS_DEV: true, DATABASE_URL: Redacted.make(harness.container.getConnectionUri()), - } as ServiceMap.Service.Shape), + })), ), Effect.provide(harness.dbLayer), ), @@ -62,7 +62,7 @@ const makeSideEffectService = (calls: SideEffectCall[], options: SideEffectOptio let messageUpdatedFailures = 0 let messageDeletedFailures = 0 - return { + return serviceShape({ handleMessageCreated: (payload: MessageCreatedPayload, dedupeKey: string) => Effect.sync(() => { calls.push({ eventType: "message_created", payload, dedupeKey }) @@ -93,7 +93,7 @@ const makeSideEffectService = (calls: SideEffectCall[], options: SideEffectOptio Effect.sync(() => { calls.push({ eventType: "reaction_deleted", payload, dedupeKey }) }), - } as ServiceMap.Service.Shape + }) } const waitFor = async (predicate: () => Promise, timeoutMs = 8_000) => { diff --git a/apps/backend/src/services/message-side-effect-service.test.ts b/apps/backend/src/services/message-side-effect-service.test.ts index 02fa2f721..abe8d2449 100644 --- a/apps/backend/src/services/message-side-effect-service.test.ts +++ b/apps/backend/src/services/message-side-effect-service.test.ts @@ -1,27 +1,27 @@ -import { FetchHttpClient } from "effect/unstable/http" +import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http" import { randomUUID } from "node:crypto" import { Database, schema } from "@hazel/db" import type { ChannelId, MessageId, MessageReactionId, OrganizationId, UserId } from "@hazel/schema" import { Effect, Layer, ServiceMap } from "effect" import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import { createChatSyncDbHarness, type ChatSyncDbHarness } from "../test/chat-sync-db-harness" -import { buildServiceLayer, configLayer } from "../test/effect-helpers" +import { configLayer, serviceShape } from "../test/effect-helpers" import { DiscordSyncWorker } from "./chat-sync/discord-sync-worker" import { MessageSideEffectService } from "./message-side-effect-service" const CLUSTER_URL = "http://cluster.test" -const ORG_ID = "00000000-0000-0000-0000-000000000001" as OrganizationId -const CHANNEL_ID = "00000000-0000-0000-0000-000000000002" as ChannelId -const THREAD_CHANNEL_ID = "00000000-0000-0000-0000-000000000003" as ChannelId -const AUTHOR_ID = "00000000-0000-0000-0000-000000000004" as UserId -const INTEGRATION_BOT_ID = "00000000-0000-0000-0000-000000000005" as UserId -const ORIGINAL_MESSAGE_ID = "00000000-0000-0000-0000-000000000006" as MessageId +const ORG_ID = "00000000-0000-4000-8000-000000000001" as OrganizationId +const CHANNEL_ID = "00000000-0000-4000-8000-000000000002" as ChannelId +const THREAD_CHANNEL_ID = "00000000-0000-4000-8000-000000000003" as ChannelId +const AUTHOR_ID = "00000000-0000-4000-8000-000000000004" as UserId +const INTEGRATION_BOT_ID = "00000000-0000-4000-8000-000000000005" as UserId +const ORIGINAL_MESSAGE_ID = "00000000-0000-4000-8000-000000000006" as MessageId const EXISTING_THREAD_MESSAGE_IDS = [ - "00000000-0000-0000-0000-000000000007" as MessageId, - "00000000-0000-0000-0000-000000000008" as MessageId, - "00000000-0000-0000-0000-000000000009" as MessageId, + "00000000-0000-4000-8000-000000000007" as MessageId, + "00000000-0000-4000-8000-000000000008" as MessageId, + "00000000-0000-4000-8000-000000000009" as MessageId, ] as const satisfies ReadonlyArray -const NEW_THREAD_MESSAGE_ID = "00000000-0000-0000-0000-000000000010" as MessageId +const NEW_THREAD_MESSAGE_ID = "00000000-0000-4000-8000-000000000010" as MessageId type DiscordCall = | { method: "message_create"; id: string; dedupeKey?: string } @@ -36,23 +36,24 @@ type WorkerOptions = { const runServiceEffect = ( harness: ChatSyncDbHarness, - worker: DiscordSyncWorker, + worker: ServiceMap.Service.Shape, make: Effect.Effect, + httpClientLayer: Layer.Layer = FetchHttpClient.layer, ) => Effect.runPromise( Effect.scoped( make.pipe( - Effect.provide(buildServiceLayer(MessageSideEffectService)), + Effect.provide(Layer.effect(MessageSideEffectService, MessageSideEffectService.make)), Effect.provide(Layer.succeed(DiscordSyncWorker, worker)), Effect.provide(configLayer({ CLUSTER_URL })), - Effect.provide(FetchHttpClient.layer), + Effect.provide(httpClientLayer), Effect.provide(harness.dbLayer), ), ) as Effect.Effect, ) const makeDiscordWorker = (calls: DiscordCall[], options: WorkerOptions = {}) => - ({ + serviceShape({ syncHazelMessageCreateToAllConnections: (messageId: string, dedupeKey?: string) => options.failCreate ? Effect.fail(new Error("discord create failed")) @@ -88,7 +89,18 @@ const makeDiscordWorker = (calls: DiscordCall[], options: WorkerOptions = {}) => calls.push({ method: "reaction_delete", payload, dedupeKey }) return { synced: 1, failed: 0 } }), - }) as ServiceMap.Service.Shape + }) + +const workflowClientLayer = (requests: Array<{ url: string }>) => + Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request, url) => + Effect.sync(() => { + requests.push({ url: String(url) }) + return HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })) + }), + ), + ) const seedMessageSideEffectState = (harness: ChatSyncDbHarness) => harness.run( @@ -215,38 +227,26 @@ describe("MessageSideEffectService", () => { it("routes message creates through Discord sync, notifications, and thread naming", async () => { const calls: DiscordCall[] = [] - const requests: Array<{ url: string; body: string | null }> = [] - const originalFetch = globalThis.fetch - - globalThis.fetch = (async (input, init) => { - requests.push({ - url: String(input), - body: typeof init?.body === "string" ? init.body : null, - }) - return new Response(null, { status: 204 }) - }) as typeof fetch + const requests: Array<{ url: string }> = [] - try { - await runServiceEffect( - harness, - makeDiscordWorker(calls), - Effect.gen(function* () { - const service = yield* MessageSideEffectService - yield* service.handleMessageCreated( - { - messageId: NEW_THREAD_MESSAGE_ID, - channelId: THREAD_CHANNEL_ID, - authorId: AUTHOR_ID, - content: "Newest thread message", - replyToMessageId: null, - }, - "dedupe-1", - ) - }), - ) - } finally { - globalThis.fetch = originalFetch - } + await runServiceEffect( + harness, + makeDiscordWorker(calls), + Effect.gen(function* () { + const service = yield* MessageSideEffectService + yield* service.handleMessageCreated( + { + messageId: NEW_THREAD_MESSAGE_ID, + channelId: THREAD_CHANNEL_ID, + authorId: AUTHOR_ID, + content: "Newest thread message", + replyToMessageId: null, + }, + "dedupe-1", + ) + }), + workflowClientLayer(requests), + ) expect(calls).toEqual([ { @@ -255,85 +255,65 @@ describe("MessageSideEffectService", () => { dedupeKey: "dedupe-1", }, ]) - expect(requests).toHaveLength(2) expect(requests.every((request) => request.url.startsWith(CLUSTER_URL))).toBe(true) - expect(requests.map((request) => request.url).join("\n")).toContain("message-notification-workflow") - expect(requests.map((request) => request.url).join("\n")).toContain("thread-naming-workflow") + const workflowUrls = requests.map((request) => request.url).join("\n") + expect(workflowUrls).toContain("messagenotificationworkflow") + expect(workflowUrls).toContain("threadnamingworkflow") }) it("skips Discord loopback for the integration bot but still runs workflows", async () => { const calls: DiscordCall[] = [] - const requests: Array<{ url: string; body: string | null }> = [] - const originalFetch = globalThis.fetch - - globalThis.fetch = (async (input, init) => { - requests.push({ - url: String(input), - body: typeof init?.body === "string" ? init.body : null, - }) - return new Response(null, { status: 204 }) - }) as typeof fetch + const requests: Array<{ url: string }> = [] - try { - await runServiceEffect( - harness, - makeDiscordWorker(calls), - Effect.gen(function* () { - const service = yield* MessageSideEffectService - yield* service.handleMessageCreated( - { - messageId: randomUUID() as MessageId, - channelId: CHANNEL_ID, - authorId: INTEGRATION_BOT_ID, - content: "integration message", - replyToMessageId: null, - }, - "dedupe-2", - ) - }), - ) - } finally { - globalThis.fetch = originalFetch - } + await runServiceEffect( + harness, + makeDiscordWorker(calls), + Effect.gen(function* () { + const service = yield* MessageSideEffectService + yield* service.handleMessageCreated( + { + messageId: randomUUID() as MessageId, + channelId: CHANNEL_ID, + authorId: INTEGRATION_BOT_ID, + content: "integration message", + replyToMessageId: null, + }, + "dedupe-2", + ) + }), + workflowClientLayer(requests), + ) expect(calls).toHaveLength(0) expect(requests).toHaveLength(1) - expect(requests[0]?.url).toContain("message-notification-workflow") + expect(requests[0]?.url).toContain("messagenotificationworkflow") }) it("continues to workflows when Discord create sync fails", async () => { - const requests: Array = [] - const originalFetch = globalThis.fetch - - globalThis.fetch = (async (input) => { - requests.push(String(input)) - return new Response(null, { status: 204 }) - }) as typeof fetch + const requests: Array<{ url: string }> = [] - try { - await runServiceEffect( - harness, - makeDiscordWorker([], { failCreate: true }), - Effect.gen(function* () { - const service = yield* MessageSideEffectService - yield* service.handleMessageCreated( - { - messageId: randomUUID() as MessageId, - channelId: CHANNEL_ID, - authorId: AUTHOR_ID, - content: "still notifies", - replyToMessageId: null, - }, - "dedupe-3", - ) - }), - ) - } finally { - globalThis.fetch = originalFetch - } + await runServiceEffect( + harness, + makeDiscordWorker([], { failCreate: true }), + Effect.gen(function* () { + const service = yield* MessageSideEffectService + yield* service.handleMessageCreated( + { + messageId: randomUUID() as MessageId, + channelId: CHANNEL_ID, + authorId: AUTHOR_ID, + content: "still notifies", + replyToMessageId: null, + }, + "dedupe-3", + ) + }), + workflowClientLayer(requests), + ) expect(requests).toHaveLength(1) + expect(requests[0]?.url).toContain("messagenotificationworkflow") }) it("routes message updates, deletes, and reactions to the correct Discord worker methods", async () => { @@ -359,7 +339,7 @@ describe("MessageSideEffectService", () => { ) yield* service.handleReactionCreated( { - reactionId: "00000000-0000-0000-0000-000000000011" as MessageReactionId, + reactionId: "00000000-0000-4000-8000-000000000011" as MessageReactionId, }, "dedupe-reaction-create", ) @@ -388,7 +368,7 @@ describe("MessageSideEffectService", () => { }, { method: "reaction_create", - id: "00000000-0000-0000-0000-000000000011" as MessageReactionId, + id: "00000000-0000-4000-8000-000000000011" as MessageReactionId, dedupeKey: "dedupe-reaction-create", }, { diff --git a/apps/backend/src/services/org-resolver.test.ts b/apps/backend/src/services/org-resolver.test.ts index f6f8024ec..d4b7efac3 100644 --- a/apps/backend/src/services/org-resolver.test.ts +++ b/apps/backend/src/services/org-resolver.test.ts @@ -6,13 +6,13 @@ import { Effect, Result, Layer, Option, ServiceMap } from "effect" import { OrgResolver } from "./org-resolver" import { makeActor, TEST_ORG_ID } from "../policies/policy-test-helpers" import { CurrentUser } from "@hazel/domain" -import { buildServiceLayer, serviceEffect, serviceShape } from "../test/effect-helpers" +import { serviceShape } from "../test/effect-helpers" type Role = "admin" | "member" | "owner" -const CHANNEL_ID = "00000000-0000-0000-0000-000000000501" as ChannelId -const MESSAGE_ID = "00000000-0000-0000-0000-000000000601" as MessageId -const CHANNEL_MEMBER_ID = "00000000-0000-0000-0000-000000000701" as ChannelMemberId +const CHANNEL_ID = "00000000-0000-4000-8000-000000000501" as ChannelId +const MESSAGE_ID = "00000000-0000-4000-8000-000000000601" as MessageId +const CHANNEL_MEMBER_ID = "00000000-0000-4000-8000-000000000701" as ChannelMemberId const makeOrgMemberRepoLayer = (members: Record) => Layer.succeed(OrganizationMemberRepo, serviceShape({ @@ -62,7 +62,7 @@ const makeResolverLayer = (opts: { channelMembers?: Record messages?: Record }) => - buildServiceLayer(OrgResolver).pipe( + Layer.effect(OrgResolver, OrgResolver.make).pipe( Layer.provide(makeOrgMemberRepoLayer(opts.members ?? {})), Layer.provide(makeChannelRepoLayer(opts.channels ?? {})), Layer.provide(makeChannelMemberRepoLayer(opts.channelMembers ?? {})), @@ -75,12 +75,16 @@ const runEither = ( actor: CurrentUser.Schema = makeActor(), ) => Effect.runPromise( - make.pipe(Effect.provide(layer), Effect.provideService(CurrentUser.Context, actor), Effect.result), + make.pipe( + Effect.provide(layer), + Effect.provideService(CurrentUser.Context, actor), + Effect.result, + ) as Effect.Effect, ) const use = ( fn: (resolver: ServiceMap.Service.Shape) => Effect.Effect, -) => serviceEffect(OrgResolver, fn) +) => OrgResolver.use(fn) describe("OrgResolver", () => { describe("requireScope", () => { @@ -170,7 +174,7 @@ describe("OrgResolver", () => { describe("requireOwner", () => { it("grants access for owner only", async () => { const actor = makeActor() - const adminActor = makeActor({ id: "00000000-0000-0000-0000-000000000502" as UserId }) + const adminActor = makeActor({ id: "00000000-0000-4000-8000-000000000502" as UserId }) const layer = makeResolverLayer({ members: { @@ -223,7 +227,7 @@ describe("OrgResolver", () => { members: { [`${TEST_ORG_ID}:${actor.id}`]: "member" }, }) - const missingChannelId = "00000000-0000-0000-0000-000000000599" as ChannelId + const missingChannelId = "00000000-0000-4000-8000-000000000599" as ChannelId const result = await runEither( use((r) => r.fromChannel(missingChannelId, "channels:read", "Channel", "read")), layer, @@ -322,7 +326,7 @@ describe("OrgResolver", () => { it("allows direct channel only for channel members", async () => { const actor = makeActor() - const outsider = makeActor({ id: "00000000-0000-0000-0000-000000000503" as UserId }) + const outsider = makeActor({ id: "00000000-0000-4000-8000-000000000503" as UserId }) const layer = makeResolverLayer({ members: { @@ -356,8 +360,8 @@ describe("OrgResolver", () => { it("checks parent channel access for threads", async () => { const actor = makeActor() - const parentChannelId = "00000000-0000-0000-0000-000000000502" as ChannelId - const threadId = "00000000-0000-0000-0000-000000000503" as ChannelId + const parentChannelId = "00000000-0000-4000-8000-000000000502" as ChannelId + const threadId = "00000000-0000-4000-8000-000000000503" as ChannelId const layer = makeResolverLayer({ members: { [`${TEST_ORG_ID}:${actor.id}`]: "member" }, @@ -416,7 +420,7 @@ describe("OrgResolver", () => { members: { [`${TEST_ORG_ID}:${actor.id}`]: "member" }, }) - const missingId = "00000000-0000-0000-0000-000000000699" as MessageId + const missingId = "00000000-0000-4000-8000-000000000699" as MessageId const result = await runEither( use((r) => r.fromMessage(missingId, "messages:read", "Message", "read")), layer, diff --git a/apps/backend/src/test/chat-sync-db-harness.ts b/apps/backend/src/test/chat-sync-db-harness.ts index c7908799b..b554bf947 100644 --- a/apps/backend/src/test/chat-sync-db-harness.ts +++ b/apps/backend/src/test/chat-sync-db-harness.ts @@ -1,10 +1,22 @@ -import { execSync } from "node:child_process" +import { execFileSync } from "node:child_process" import { fileURLToPath } from "node:url" -import { PostgreSqlContainer, type StartedPostgreSqlContainer } from "@testcontainers/postgresql" import { Database } from "@hazel/db" import { Effect, Layer, Redacted } from "effect" const DB_PACKAGE_DIR = fileURLToPath(new URL("../../../../packages/db", import.meta.url)) +const POSTGRES_IMAGE = "postgres:17-alpine" +const POSTGRES_USER = "user" +const POSTGRES_PASSWORD = "password" +const POSTGRES_DB = "app" +const STARTUP_TIMEOUT_MS = 60_000 +const POLL_INTERVAL_MS = 500 +const REQUIRED_TABLES = [ + "chat_sync_event_receipts", + "chat_sync_message_links", + "chat_sync_channel_links", + "chat_sync_connections", + "message_outbox_events", +] as const const TRUNCATE_SQL = ` TRUNCATE TABLE @@ -24,7 +36,9 @@ RESTART IDENTITY CASCADE; ` export interface ChatSyncDbHarness { - readonly container: StartedPostgreSqlContainer + readonly container: { + getConnectionUri: () => string + } readonly dbLayer: Layer.Layer run: (effect: Effect.Effect) => Promise reset: () => Promise @@ -32,7 +46,7 @@ export interface ChatSyncDbHarness { } const runDbPush = (databaseUrl: string) => { - execSync("bun run db:push", { + execFileSync("bun", ["run", "db:push"], { cwd: DB_PACKAGE_DIR, stdio: "pipe", env: { @@ -42,37 +56,156 @@ const runDbPush = (databaseUrl: string) => { }) } -export const createChatSyncDbHarness = async (): Promise => { - const container = await new PostgreSqlContainer("postgres:alpine").start() - const databaseUrl = container.getConnectionUri() +const runDocker = (args: ReadonlyArray, options?: { stdio?: "pipe" | "ignore" }) => + execFileSync("docker", [...args], { + encoding: "utf8", + stdio: options?.stdio ?? "pipe", + }) - runDbPush(databaseUrl) +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) +const waitForPostgres = async (containerId: string) => { + const deadline = Date.now() + STARTUP_TIMEOUT_MS + + while (Date.now() < deadline) { + try { + runDocker(["exec", containerId, "pg_isready", "-U", POSTGRES_USER, "-d", POSTGRES_DB], { + stdio: "ignore", + }) + return + } catch { + await sleep(POLL_INTERVAL_MS) + } + } + + throw new Error(`Timed out waiting for postgres container ${containerId} to become ready`) +} + +const getPublishedPort = (containerId: string) => { + const portBinding = runDocker(["port", containerId, "5432/tcp"]).trim() + const hostPort = portBinding.split(":").at(-1) + + if (!hostPort) { + throw new Error(`Could not determine published port for postgres container ${containerId}`) + } + + return hostPort +} + +const ensureSchema = async (databaseUrl: string) => { const dbLayer = Database.layer({ url: Redacted.make(databaseUrl), ssl: false, }) - const run = (effect: Effect.Effect) => - Effect.runPromise((effect as Effect.Effect).pipe(Effect.provide(dbLayer), Effect.scoped)) + const existingTables = await Effect.runPromise( + Effect.gen(function* () { + const db = yield* Database.Database + return yield* db.execute((client) => + client.$client.unsafe( + "select tablename from pg_tables where schemaname = 'public' order by tablename", + ), + ) + }).pipe(Effect.provide(dbLayer), Effect.scoped), + ) - const reset = () => - run( - Effect.gen(function* () { - const db = yield* Database.Database - yield* db.execute((client) => client.$client.unsafe(TRUNCATE_SQL)) - }), - ) + const tableSet = new Set( + existingTables.flatMap((row) => + typeof row === "object" && row !== null && "tablename" in row + ? [String((row as unknown as { tablename: unknown }).tablename)] + : [], + ), + ) - const stop = async () => { - await container.stop() + const missingTables = REQUIRED_TABLES.filter((table) => !tableSet.has(table)) + if (missingTables.length === 0) { + return } - return { - container, - dbLayer, - run, - reset, - stop, + runDbPush(databaseUrl) + + const recheckedTables = await Effect.runPromise( + Effect.gen(function* () { + const db = yield* Database.Database + return yield* db.execute((client) => + client.$client.unsafe( + "select tablename from pg_tables where schemaname = 'public' order by tablename", + ), + ) + }).pipe(Effect.provide(dbLayer), Effect.scoped), + ) + + const recheckedSet = new Set( + recheckedTables.flatMap((row) => + typeof row === "object" && row !== null && "tablename" in row + ? [String((row as unknown as { tablename: unknown }).tablename)] + : [], + ), + ) + + const stillMissing = REQUIRED_TABLES.filter((table) => !recheckedSet.has(table)) + if (stillMissing.length > 0) { + throw new Error(`Chat sync test schema is incomplete: missing ${stillMissing.join(", ")}`) + } +} + +export const createChatSyncDbHarness = async (): Promise => { + const containerId = runDocker([ + "run", + "-d", + "--rm", + "-e", + `POSTGRES_USER=${POSTGRES_USER}`, + "-e", + `POSTGRES_PASSWORD=${POSTGRES_PASSWORD}`, + "-e", + `POSTGRES_DB=${POSTGRES_DB}`, + "-p", + "127.0.0.1::5432", + POSTGRES_IMAGE, + ]).trim() + + try { + await waitForPostgres(containerId) + const hostPort = getPublishedPort(containerId) + const databaseUrl = `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:${hostPort}/${POSTGRES_DB}?sslmode=disable` + + runDbPush(databaseUrl) + await ensureSchema(databaseUrl) + + const dbLayer = Database.layer({ + url: Redacted.make(databaseUrl), + ssl: false, + }) + + const run = (effect: Effect.Effect) => + Effect.runPromise( + (effect as Effect.Effect).pipe(Effect.provide(dbLayer), Effect.scoped), + ) + + const reset = () => + run( + Effect.gen(function* () { + const db = yield* Database.Database + yield* db.execute((client) => client.$client.unsafe(TRUNCATE_SQL)) + }), + ) + + const stop = async () => { + runDocker(["rm", "-f", containerId], { stdio: "ignore" }) + } + + return { + container: { + getConnectionUri: () => databaseUrl, + }, + dbLayer, + run, + reset, + stop, + } + } catch (error) { + runDocker(["rm", "-f", containerId], { stdio: "ignore" }) + throw error } } diff --git a/apps/backend/src/test/effect-helpers.ts b/apps/backend/src/test/effect-helpers.ts index 7f72c9b85..6df6c713f 100644 --- a/apps/backend/src/test/effect-helpers.ts +++ b/apps/backend/src/test/effect-helpers.ts @@ -1,18 +1,7 @@ -import { ConfigProvider, Effect, Layer, ServiceMap } from "effect" - -export const buildServiceLayer = ( - service: ServiceMap.Service & { - readonly make: Effect.Effect - }, -) => Layer.effect(service, service.make) - -export const serviceEffect = ( - service: ServiceMap.Service, - f: (implementation: S) => Effect.Effect, -) => service.use(f) +import { ConfigProvider, ServiceMap } from "effect" export const serviceShape = ( - shape: Partial>, + shape: unknown, ) => shape as ServiceMap.Service.Shape export const configLayer = (values: Record) => diff --git a/apps/backend/src/test/message-outbox-repo.test.ts b/apps/backend/src/test/message-outbox-repo.test.ts index ccda68bb6..5c7fa5a35 100644 --- a/apps/backend/src/test/message-outbox-repo.test.ts +++ b/apps/backend/src/test/message-outbox-repo.test.ts @@ -6,17 +6,17 @@ import { Effect } from "effect" import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import { createChatSyncDbHarness, type ChatSyncDbHarness } from "./chat-sync-db-harness" -const CHANNEL_ID = "00000000-0000-0000-0000-000000000001" as ChannelId -const AUTHOR_ID = "00000000-0000-0000-0000-000000000102" as UserId -const MESSAGE_ID_1 = "00000000-0000-0000-0000-000000000101" as MessageId -const MESSAGE_ID_2 = "00000000-0000-0000-0000-000000000103" as MessageId -const MESSAGE_ID_3 = "00000000-0000-0000-0000-000000000105" as MessageId -const MESSAGE_ID_4 = "00000000-0000-0000-0000-000000000106" as MessageId -const MESSAGE_ID_5 = "00000000-0000-0000-0000-000000000107" as MessageId -const MESSAGE_ID_6 = "00000000-0000-0000-0000-000000000109" as MessageId -const REACTION_ID = "00000000-0000-0000-0000-000000000104" as MessageReactionId -const REACTION_USER_ID = "00000000-0000-0000-0000-000000000108" as UserId -const SECOND_AUTHOR_ID = "00000000-0000-0000-0000-000000000110" as UserId +const CHANNEL_ID = "00000000-0000-4000-8000-000000000001" as ChannelId +const AUTHOR_ID = "00000000-0000-4000-8000-000000000102" as UserId +const MESSAGE_ID_1 = "00000000-0000-4000-8000-000000000101" as MessageId +const MESSAGE_ID_2 = "00000000-0000-4000-8000-000000000103" as MessageId +const MESSAGE_ID_3 = "00000000-0000-4000-8000-000000000105" as MessageId +const MESSAGE_ID_4 = "00000000-0000-4000-8000-000000000106" as MessageId +const MESSAGE_ID_5 = "00000000-0000-4000-8000-000000000107" as MessageId +const MESSAGE_ID_6 = "00000000-0000-4000-8000-000000000109" as MessageId +const REACTION_ID = "00000000-0000-4000-8000-000000000104" as MessageReactionId +const REACTION_USER_ID = "00000000-0000-4000-8000-000000000108" as UserId +const SECOND_AUTHOR_ID = "00000000-0000-4000-8000-000000000110" as UserId const uuid = () => randomUUID() diff --git a/apps/docs/src/routeTree.gen.ts b/apps/docs/src/routeTree.gen.ts index f9984f5d6..f759073a6 100644 --- a/apps/docs/src/routeTree.gen.ts +++ b/apps/docs/src/routeTree.gen.ts @@ -8,77 +8,79 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { Route as rootRouteImport } from "./routes/__root" -import { Route as SplatRouteImport } from "./routes/$" -import { Route as ApiSearchRouteImport } from "./routes/api/search" +import { Route as rootRouteImport } from './routes/__root' +import { Route as SplatRouteImport } from './routes/$' +import { Route as ApiSearchRouteImport } from './routes/api/search' const SplatRoute = SplatRouteImport.update({ - id: "/$", - path: "/$", - getParentRoute: () => rootRouteImport, + id: '/$', + path: '/$', + getParentRoute: () => rootRouteImport, } as any) const ApiSearchRoute = ApiSearchRouteImport.update({ - id: "/api/search", - path: "/api/search", - getParentRoute: () => rootRouteImport, + id: '/api/search', + path: '/api/search', + getParentRoute: () => rootRouteImport, } as any) export interface FileRoutesByFullPath { - "/$": typeof SplatRoute - "/api/search": typeof ApiSearchRoute + '/$': typeof SplatRoute + '/api/search': typeof ApiSearchRoute } export interface FileRoutesByTo { - "/$": typeof SplatRoute - "/api/search": typeof ApiSearchRoute + '/$': typeof SplatRoute + '/api/search': typeof ApiSearchRoute } export interface FileRoutesById { - __root__: typeof rootRouteImport - "/$": typeof SplatRoute - "/api/search": typeof ApiSearchRoute + __root__: typeof rootRouteImport + '/$': typeof SplatRoute + '/api/search': typeof ApiSearchRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: "/$" | "/api/search" - fileRoutesByTo: FileRoutesByTo - to: "/$" | "/api/search" - id: "__root__" | "/$" | "/api/search" - fileRoutesById: FileRoutesById + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/$' | '/api/search' + fileRoutesByTo: FileRoutesByTo + to: '/$' | '/api/search' + id: '__root__' | '/$' | '/api/search' + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - SplatRoute: typeof SplatRoute - ApiSearchRoute: typeof ApiSearchRoute + SplatRoute: typeof SplatRoute + ApiSearchRoute: typeof ApiSearchRoute } -declare module "@tanstack/react-router" { - interface FileRoutesByPath { - "/$": { - id: "/$" - path: "/$" - fullPath: "/$" - preLoaderRoute: typeof SplatRouteImport - parentRoute: typeof rootRouteImport - } - "/api/search": { - id: "/api/search" - path: "/api/search" - fullPath: "/api/search" - preLoaderRoute: typeof ApiSearchRouteImport - parentRoute: typeof rootRouteImport - } - } +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/$': { + id: '/$' + path: '/$' + fullPath: '/$' + preLoaderRoute: typeof SplatRouteImport + parentRoute: typeof rootRouteImport + } + '/api/search': { + id: '/api/search' + path: '/api/search' + fullPath: '/api/search' + preLoaderRoute: typeof ApiSearchRouteImport + parentRoute: typeof rootRouteImport + } + } } const rootRouteChildren: RootRouteChildren = { - SplatRoute: SplatRoute, - ApiSearchRoute: ApiSearchRoute, + SplatRoute: SplatRoute, + ApiSearchRoute: ApiSearchRoute, } -export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes() +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() -import type { getRouter } from "./router.tsx" -import type { createStart } from "@tanstack/react-start" -declare module "@tanstack/react-start" { - interface Register { - ssr: true - router: Awaited> - } +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } } diff --git a/apps/web/src/atoms/desktop-auth.ts b/apps/web/src/atoms/desktop-auth.ts index 3ac1fe904..1196f9d0f 100644 --- a/apps/web/src/atoms/desktop-auth.ts +++ b/apps/web/src/atoms/desktop-auth.ts @@ -10,7 +10,7 @@ import { Atom } from "effect/unstable/reactivity" import { Clipboard } from "@effect/platform-browser" import type { OrganizationId } from "@hazel/schema" -import { Duration, Effect, Layer, Option, Schema } from "effect" +import { Duration, Effect, Layer, Option, Schema, type ServiceMap } from "effect" import { appRegistry } from "~/lib/registry" import { runtime } from "~/lib/services/common/runtime" import { TauriAuth } from "~/lib/services/desktop/tauri-auth" @@ -24,26 +24,26 @@ import { forceRefreshEffect, getAccessToken as getAccessTokenPromise } from "~/l // ============================================================================ export interface DesktopTokens { - accessToken: string - refreshToken: string - expiresAt: number + accessToken: string + refreshToken: string + expiresAt: number } export type DesktopAuthStatus = "idle" | "loading" | "authenticated" | "error" export interface DesktopAuthError { - _tag: string - message: string + _tag: string + message: string } interface DesktopLoginOptions { - returnTo?: string - organizationId?: OrganizationId - invitationToken?: string + returnTo?: string + organizationId?: OrganizationId + invitationToken?: string } interface DesktopLogoutOptions { - redirectTo?: string + redirectTo?: string } // ============================================================================ @@ -61,6 +61,10 @@ const TokenExchangeLive = TokenExchange.layer const TauriAuthLive = TauriAuth.layer const ClipboardLive = Clipboard.layer +type TauriAuthService = ServiceMap.Service.Shape +type TokenStorageService = ServiceMap.Service.Shape +type TokenExchangeService = ServiceMap.Service.Shape + // ============================================================================ // Core State Atoms // ============================================================================ @@ -75,6 +79,18 @@ export const desktopAuthErrorAtom = Atom.make(null).pip export const isDesktopAuthenticatedAtom = Atom.make((get) => get(desktopTokensAtom) !== null) +// ============================================================================ +// Helper to extract error info +// ============================================================================ + +function toErrorInfo(error: unknown): { _tag: string; message: string } { + const e = error as { _tag?: string; message?: string } | undefined + return { + _tag: e?._tag ?? "UnknownError", + message: e?.message ?? "Unknown error", + } +} + // ============================================================================ // Action Atoms // ============================================================================ @@ -83,160 +99,167 @@ export const isDesktopAuthenticatedAtom = Atom.make((get) => get(desktopTokensAt * Action atom that initiates the desktop OAuth flow */ export const desktopLoginAtom = Atom.fn( - Effect.fnUntraced(function* (options: DesktopLoginOptions, get) { - if (!isTauri()) { - yield* Effect.log("[desktop-auth] Not in Tauri environment, skipping desktop login") - return - } - - get.set(desktopAuthStatusAtom, "loading") - get.set(desktopAuthErrorAtom, null) - - const result = yield* Effect.gen(function* () { - const auth = yield* TauriAuth - const authResult = yield* auth.initiateAuth(options) - - const tokenStorage = yield* TokenStorage - const accessTokenOpt = yield* tokenStorage.getAccessToken - const refreshTokenOpt = yield* tokenStorage.getRefreshToken - const expiresAtOpt = yield* tokenStorage.getExpiresAt - - if ( - Option.isSome(accessTokenOpt) && - Option.isSome(refreshTokenOpt) && - Option.isSome(expiresAtOpt) - ) { - get.set(desktopTokensAtom, { - accessToken: accessTokenOpt.value, - refreshToken: refreshTokenOpt.value, - expiresAt: expiresAtOpt.value, - }) - get.set(desktopAuthStatusAtom, "authenticated") - } - - return authResult - }).pipe( - Effect.provide(Layer.mergeAll(TauriAuthLive, TokenStorageLive)), - Effect.catch((error) => { - console.error("[desktop-auth] Login failed:", error) - get.set(desktopAuthStatusAtom, "error") - get.set(desktopAuthErrorAtom, { - _tag: error._tag ?? "UnknownError", - message: error.message ?? "Login failed", - }) - return Effect.fail(error) - }), - ) - - yield* Effect.log(`[desktop-auth] Login successful, navigating to: ${result.returnTo}`) - window.location.href = result.returnTo - }), + Effect.fnUntraced(function* (options: DesktopLoginOptions | undefined, get) { + if (!isTauri()) { + yield* Effect.log("[desktop-auth] Not in Tauri environment, skipping desktop login") + return + } + + get.set(desktopAuthStatusAtom, "loading") + get.set(desktopAuthErrorAtom, null) + + const loginEffect = Effect.gen(function* () { + const auth: TauriAuthService = yield* TauriAuth + const authResult = yield* auth.initiateAuth(options) + + const tokenStorage: TokenStorageService = yield* TokenStorage + const accessTokenOpt = yield* tokenStorage.getAccessToken + const refreshTokenOpt = yield* tokenStorage.getRefreshToken + const expiresAtOpt = yield* tokenStorage.getExpiresAt + + if ( + Option.isSome(accessTokenOpt) && + Option.isSome(refreshTokenOpt) && + Option.isSome(expiresAtOpt) + ) { + get.set(desktopTokensAtom, { + accessToken: accessTokenOpt.value, + refreshToken: refreshTokenOpt.value, + expiresAt: expiresAtOpt.value, + }) + get.set(desktopAuthStatusAtom, "authenticated") + } + + return authResult + }).pipe(Effect.provide(Layer.mergeAll(TauriAuthLive, TokenStorageLive))) + + const result = yield* loginEffect.pipe( + Effect.catch((error) => { + console.error("[desktop-auth] Login failed:", error) + const info = toErrorInfo(error) + get.set(desktopAuthStatusAtom, "error") + get.set(desktopAuthErrorAtom, info) + return Effect.fail(error) + }), + ) + + yield* Effect.log(`[desktop-auth] Login successful, navigating to: ${result.returnTo}`) + window.location.href = result.returnTo + }), ) /** * Action atom that performs desktop logout */ export const desktopLogoutAtom = Atom.fn( - Effect.fnUntraced(function* (options?: DesktopLogoutOptions, get?) { - if (!isTauri()) { - yield* Effect.log("[desktop-auth] Not in Tauri environment, skipping desktop logout") - return - } - - yield* Effect.gen(function* () { - const tokenStorage = yield* TokenStorage - yield* tokenStorage.clearTokens - }).pipe( - Effect.provide(TokenStorageLive), - Effect.catch((error) => { - console.error("[desktop-auth] Failed to clear tokens:", error) - return Effect.void - }), - ) - - get?.set(desktopTokensAtom, null) - get?.set(desktopAuthStatusAtom, "idle") - get?.set(desktopAuthErrorAtom, null) - - const redirectTo = options?.redirectTo || "/" - yield* Effect.log(`[desktop-auth] Logout complete, redirecting to: ${redirectTo}`) - window.location.href = redirectTo - }), + Effect.fnUntraced(function* (options: DesktopLogoutOptions | undefined, get) { + if (!isTauri()) { + yield* Effect.log("[desktop-auth] Not in Tauri environment, skipping desktop logout") + return + } + + yield* Effect.gen(function* () { + const tokenStorage: TokenStorageService = yield* TokenStorage + yield* tokenStorage.clearTokens + }).pipe( + Effect.provide(TokenStorageLive), + Effect.catch(() => { + console.error("[desktop-auth] Failed to clear tokens") + return Effect.void + }), + ) + + get.set(desktopTokensAtom, null) + get.set(desktopAuthStatusAtom, "idle") + get.set(desktopAuthErrorAtom, null) + + const redirectTo = options?.redirectTo || "/" + yield* Effect.log(`[desktop-auth] Logout complete, redirecting to: ${redirectTo}`) + window.location.href = redirectTo + }), ) /** * Action atom that forces an immediate token refresh via AuthToken */ export const desktopForceRefreshAtom = Atom.fn( - Effect.fnUntraced(function* (_: void) { - if (!isTauri()) return false - return yield* forceRefreshEffect - }), + Effect.fnUntraced(function* (_: void) { + if (!isTauri()) return false + return yield* forceRefreshEffect + }), ) /** * Schema for clipboard auth payload */ const ClipboardAuthPayload = Schema.Struct({ - code: Schema.String, - state: Schema.Unknown, + code: Schema.String, + state: Schema.Unknown, }) +interface ExchangeResult { + accessToken: string + refreshToken: string + expiresIn: number +} + /** * Action atom that authenticates using clipboard data */ export const desktopLoginFromClipboardAtom = Atom.fn( - Effect.fnUntraced(function* (_: void, get) { - if (!isTauri()) return - - get.set(desktopAuthStatusAtom, "loading") - get.set(desktopAuthErrorAtom, null) - - const result = yield* Effect.gen(function* () { - const clipboard = yield* Clipboard.Clipboard - const clipboardText = yield* clipboard.readString - - const rawJson = yield* Effect.try({ - try: () => JSON.parse(clipboardText), - catch: () => new Error("Invalid clipboard data - not valid JSON"), - }) - - const parsed = yield* Schema.decodeUnknownEffect(ClipboardAuthPayload)(rawJson).pipe( - Effect.mapError(() => new Error("Invalid clipboard data - missing code or state")), - ) - - const stateString = typeof parsed.state === "string" ? parsed.state : JSON.stringify(parsed.state) - - const tokenExchange = yield* TokenExchange - const tokens = yield* tokenExchange.exchangeCode(parsed.code, stateString) - - const tokenStorage = yield* TokenStorage - yield* tokenStorage.storeTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn) - - return tokens - }).pipe( - Effect.provide(Layer.mergeAll(ClipboardLive, TokenExchangeLive, TokenStorageLive)), - Effect.catch((error) => { - console.error("[desktop-auth] Clipboard login failed:", error) - get.set(desktopAuthStatusAtom, "error") - get.set(desktopAuthErrorAtom, { - _tag: "ClipboardAuthError", - message: error instanceof Error ? error.message : "Failed to authenticate from clipboard", - }) - return Effect.fail(error) - }), - ) - - get.set(desktopTokensAtom, { - accessToken: result.accessToken, - refreshToken: result.refreshToken, - expiresAt: Date.now() + result.expiresIn * 1000, - }) - get.set(desktopAuthStatusAtom, "authenticated") - - yield* Effect.log("[desktop-auth] Clipboard login successful") - window.location.href = "/" - }), + Effect.fnUntraced(function* (_: void, get) { + if (!isTauri()) return + + get.set(desktopAuthStatusAtom, "loading") + get.set(desktopAuthErrorAtom, null) + + const clipboardEffect = Effect.gen(function* () { + const clipboard = yield* Clipboard.Clipboard + const clipboardText = yield* clipboard.readString + + const rawJson = yield* Effect.try({ + try: () => JSON.parse(clipboardText), + catch: () => new Error("Invalid clipboard data - not valid JSON"), + }) + + const parsed = yield* Schema.decodeUnknownEffect(ClipboardAuthPayload)(rawJson).pipe( + Effect.mapError(() => new Error("Invalid clipboard data - missing code or state")), + ) + + const stateString = + typeof parsed.state === "string" ? parsed.state : JSON.stringify(parsed.state) + + const tokenExchange: TokenExchangeService = yield* TokenExchange + const tokens = yield* tokenExchange.exchangeCode(parsed.code, stateString) + + return tokens as ExchangeResult + }).pipe(Effect.provide(Layer.mergeAll(ClipboardLive, TokenExchangeLive, TokenStorageLive))) + + const result: ExchangeResult = yield* clipboardEffect.pipe( + Effect.catch((error) => { + console.error("[desktop-auth] Clipboard login failed:", error) + get.set(desktopAuthStatusAtom, "error") + get.set(desktopAuthErrorAtom, { + _tag: "ClipboardAuthError", + message: + error instanceof Error + ? error.message + : "Failed to authenticate from clipboard", + }) + return Effect.fail(error) + }), + ) + + get.set(desktopTokensAtom, { + accessToken: result.accessToken, + refreshToken: result.refreshToken, + expiresAt: Date.now() + result.expiresIn * 1000, + }) + get.set(desktopAuthStatusAtom, "authenticated") + + yield* Effect.log("[desktop-auth] Clipboard login successful") + window.location.href = "/" + }), ) // ============================================================================ @@ -244,46 +267,44 @@ export const desktopLoginFromClipboardAtom = Atom.fn( // ============================================================================ export const desktopInitAtom = Atom.make((get) => { - if (!isTauri()) return null - - const loadTokens = Effect.gen(function* () { - const tokenStorage = yield* TokenStorage - const accessTokenOpt = yield* tokenStorage.getAccessToken - const refreshTokenOpt = yield* tokenStorage.getRefreshToken - const expiresAtOpt = yield* tokenStorage.getExpiresAt - - if (Option.isSome(accessTokenOpt) && Option.isSome(refreshTokenOpt) && Option.isSome(expiresAtOpt)) { - get.set(desktopTokensAtom, { - accessToken: accessTokenOpt.value, - refreshToken: refreshTokenOpt.value, - expiresAt: expiresAtOpt.value, - }) - get.set(desktopAuthStatusAtom, "authenticated") - yield* Effect.log("[desktop-auth] Loaded tokens from storage") - } else { - get.set(desktopAuthStatusAtom, "idle") - yield* Effect.log("[desktop-auth] No stored tokens found") - } - }).pipe( - Effect.provide(TokenStorageLive), - Effect.catch((error) => { - console.error("[desktop-auth] Failed to load tokens:", error) - get.set(desktopAuthStatusAtom, "error") - get.set(desktopAuthErrorAtom, { - _tag: error._tag ?? "UnknownError", - message: error.message ?? "Failed to load tokens", - }) - return Effect.void - }), - ) - - const fiber = runtime.runFork(loadTokens) - - get.addFinalizer(() => { - fiber.interruptUnsafe() - }) - - return null + if (!isTauri()) return null + + const loadTokens = Effect.gen(function* () { + const tokenStorage: TokenStorageService = yield* TokenStorage + const accessTokenOpt = yield* tokenStorage.getAccessToken + const refreshTokenOpt = yield* tokenStorage.getRefreshToken + const expiresAtOpt = yield* tokenStorage.getExpiresAt + + if (Option.isSome(accessTokenOpt) && Option.isSome(refreshTokenOpt) && Option.isSome(expiresAtOpt)) { + get.set(desktopTokensAtom, { + accessToken: accessTokenOpt.value, + refreshToken: refreshTokenOpt.value, + expiresAt: expiresAtOpt.value, + }) + get.set(desktopAuthStatusAtom, "authenticated") + yield* Effect.log("[desktop-auth] Loaded tokens from storage") + } else { + get.set(desktopAuthStatusAtom, "idle") + yield* Effect.log("[desktop-auth] No stored tokens found") + } + }).pipe( + Effect.provide(TokenStorageLive), + Effect.catch((error) => { + console.error("[desktop-auth] Failed to load tokens:", error) + const info = toErrorInfo(error) + get.set(desktopAuthStatusAtom, "error") + get.set(desktopAuthErrorAtom, info) + return Effect.void + }), + ) + + const fiber = runtime.runFork(loadTokens) + + get.addFinalizer(() => { + fiber.interruptUnsafe() + }) + + return null }).pipe(Atom.keepAlive) // ============================================================================ @@ -291,39 +312,39 @@ export const desktopInitAtom = Atom.make((get) => { // ============================================================================ export const desktopTokenSchedulerAtom = Atom.make((get) => { - const tokens = get(desktopTokensAtom) + const tokens = get(desktopTokensAtom) - if (!tokens || !isTauri()) return null + if (!tokens || !isTauri()) return null - const timeUntilRefresh = tokens.expiresAt - Date.now() - REFRESH_BUFFER_MS + const timeUntilRefresh = tokens.expiresAt - Date.now() - REFRESH_BUFFER_MS - if (timeUntilRefresh <= 0) { - runtime.runFork( - Effect.gen(function* () { - yield* Effect.log("[desktop-auth] Token expired or expiring soon, refreshing now") - yield* forceRefreshEffect - }), - ) - return { scheduledFor: Date.now(), immediate: true } - } + if (timeUntilRefresh <= 0) { + runtime.runFork( + Effect.gen(function* () { + yield* Effect.log("[desktop-auth] Token expired or expiring soon, refreshing now") + yield* forceRefreshEffect + }), + ) + return { scheduledFor: Date.now(), immediate: true } + } - const minutes = Math.round(timeUntilRefresh / 1000 / 60) - const scheduledFor = tokens.expiresAt - REFRESH_BUFFER_MS + const minutes = Math.round(timeUntilRefresh / 1000 / 60) + const scheduledFor = tokens.expiresAt - REFRESH_BUFFER_MS - const refreshSchedule = Effect.gen(function* () { - yield* Effect.log(`[desktop-auth] Scheduling refresh in ${minutes} minutes`) - yield* Effect.sleep(Duration.millis(timeUntilRefresh)) - yield* Effect.log("[desktop-auth] Scheduled refresh triggered") - yield* forceRefreshEffect - }) + const refreshSchedule = Effect.gen(function* () { + yield* Effect.log(`[desktop-auth] Scheduling refresh in ${minutes} minutes`) + yield* Effect.sleep(Duration.millis(timeUntilRefresh)) + yield* Effect.log("[desktop-auth] Scheduled refresh triggered") + yield* forceRefreshEffect + }) - const fiber = runtime.runFork(refreshSchedule) + const fiber = runtime.runFork(refreshSchedule) - get.addFinalizer(() => { - fiber.interruptUnsafe() - }) + get.addFinalizer(() => { + fiber.interruptUnsafe() + }) - return { scheduledFor, immediate: false } + return { scheduledFor, immediate: false } }).pipe(Atom.keepAlive) // ============================================================================ @@ -331,30 +352,30 @@ export const desktopTokenSchedulerAtom = Atom.make((get) => { // ============================================================================ export const getDesktopAccessToken = (): Promise => { - if (!isTauri()) return Promise.resolve(null) - return getAccessTokenPromise() + if (!isTauri()) return Promise.resolve(null) + return getAccessTokenPromise() } export const clearDesktopTokens = (): Promise => { - if (!isTauri()) return Promise.resolve() - - return runtime.runPromise( - Effect.gen(function* () { - const tokenStorage = yield* TokenStorage - yield* tokenStorage.clearTokens - }).pipe( - Effect.provide(TokenStorageLive), - Effect.catch((error) => { - console.error("[desktop-auth] Failed to clear tokens during recovery:", error) - return Effect.void - }), - Effect.ensuring( - Effect.sync(() => { - appRegistry.set(desktopTokensAtom, null) - appRegistry.set(desktopAuthStatusAtom, "idle") - appRegistry.set(desktopAuthErrorAtom, null) - }), - ), - ), - ) + if (!isTauri()) return Promise.resolve() + + return runtime.runPromise( + Effect.gen(function* () { + const tokenStorage: TokenStorageService = yield* TokenStorage + yield* tokenStorage.clearTokens + }).pipe( + Effect.provide(TokenStorageLive), + Effect.catch(() => { + console.error("[desktop-auth] Failed to clear tokens during recovery") + return Effect.void + }), + Effect.ensuring( + Effect.sync(() => { + appRegistry.set(desktopTokensAtom, null) + appRegistry.set(desktopAuthStatusAtom, "idle") + appRegistry.set(desktopAuthErrorAtom, null) + }), + ), + ), + ) as Promise } diff --git a/apps/web/src/atoms/web-callback-atoms.ts b/apps/web/src/atoms/web-callback-atoms.ts index 248a69150..b1f2f8ce5 100644 --- a/apps/web/src/atoms/web-callback-atoms.ts +++ b/apps/web/src/atoms/web-callback-atoms.ts @@ -11,7 +11,7 @@ import { OAuthCodeExpiredError, TokenExchangeError, } from "@hazel/domain/errors" -import { Effect, Layer } from "effect" +import { Effect, Layer, type ServiceMap } from "effect" import { appRegistry } from "~/lib/registry" import { runtime } from "~/lib/services/common/runtime" import { TokenExchange } from "~/lib/services/desktop/token-exchange" @@ -65,6 +65,9 @@ export const webCallbackStatusAtom = Atom.make({ _tag: "idle" const WebTokenStorageLive = WebTokenStorage.layer const TokenExchangeLive = TokenExchange.layer +type TokenExchangeService = ServiceMap.Service.Shape +type WebTokenStorageService = ServiceMap.Service.Shape + // ============================================================================ // Error Handling // ============================================================================ @@ -116,7 +119,32 @@ const processedCodes = new Set() /** * Effect that handles the web callback - exchanges code for tokens and stores them */ -const handleCallback = (params: WebCallbackParams) => +const exchangeAndStoreTokens = (code: string, stateString: string, returnTo: string) => + Effect.gen(function* () { + const tokenExchange: TokenExchangeService = yield* TokenExchange + const tokenStorage: WebTokenStorageService = yield* WebTokenStorage + + yield* Effect.log("[web-callback] Exchanging code for tokens...") + + const tokens = yield* tokenExchange.exchangeCode(code, stateString) + + yield* Effect.log("[web-callback] Storing tokens...") + yield* tokenStorage.storeTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn) + + const expiresAt = Date.now() + tokens.expiresIn * 1000 + appRegistry.set(webTokensAtom, { + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + expiresAt, + }) + appRegistry.set(webAuthStatusAtom, "authenticated") + + yield* Effect.log("[web-callback] Token exchange successful") + + return { success: true as const, returnTo } + }) + +const handleCallback = (params: WebCallbackParams): Effect.Effect => Effect.gen(function* () { // Guard against double-execution (React StrictMode, hot reload) // OAuth codes are one-time use, so we track processed codes @@ -188,55 +216,34 @@ const handleCallback = (params: WebCallbackParams) => const returnTo = authState.returnTo || "/" // Exchange code for tokens - const result = yield* Effect.gen(function* () { - const tokenExchange = yield* TokenExchange - const tokenStorage = yield* WebTokenStorage - - yield* Effect.log("[web-callback] Exchanging code for tokens...") - - const tokens = yield* tokenExchange.exchangeCode(code, stateString) - - yield* Effect.log("[web-callback] Storing tokens...") - - // Store tokens in localStorage - yield* tokenStorage.storeTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn) - - // Update atom state via global registry (not `get.set()` which can be - // stale if React StrictMode unmounted the atom that forked this fiber) - const expiresAt = Date.now() + tokens.expiresIn * 1000 - appRegistry.set(webTokensAtom, { - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken, - expiresAt, - }) - appRegistry.set(webAuthStatusAtom, "authenticated") - - yield* Effect.log("[web-callback] Token exchange successful") - - return { success: true as const, returnTo } - }).pipe( + const result = yield* exchangeAndStoreTokens(code, stateString, returnTo).pipe( Effect.provide(Layer.mergeAll(TokenExchangeLive, WebTokenStorageLive)), - // Preserve typed errors (OAuthCodeExpiredError, TokenExchangeError, etc.) - Effect.catchTag("OAuthCodeExpiredError", (error) => { - console.error("[web-callback] OAuth code expired:", error) - return Effect.succeed({ - success: false as const, - error, - }) - }), - Effect.catchTag("TokenExchangeError", (error) => { - console.error("[web-callback] Token exchange failed:", error) - return Effect.succeed({ - success: false as const, - error, - }) + Effect.catchTags({ + OAuthCodeExpiredError: (error) => { + console.error("[web-callback] OAuth code expired:", error) + return Effect.succeed({ + success: false as const, + error, + }) + }, + TokenExchangeError: (error) => { + console.error("[web-callback] Token exchange failed:", error) + return Effect.succeed({ + success: false as const, + error, + }) + }, }), - Effect.catch((error) => { + Effect.catch((error: unknown) => { console.error("[web-callback] Token exchange failed:", error) + const msg = + error && typeof error === "object" && "message" in error + ? (error as { message?: string }).message + : undefined return Effect.succeed({ success: false as const, error: new TokenExchangeError({ - message: error.message || "Failed to exchange authorization code", + message: msg || "Failed to exchange authorization code", detail: String(error), }), }) diff --git a/apps/web/src/components/channel-settings/integration-card.tsx b/apps/web/src/components/channel-settings/integration-card.tsx index 7b012081a..28fe27edc 100644 --- a/apps/web/src/components/channel-settings/integration-card.tsx +++ b/apps/web/src/components/channel-settings/integration-card.tsx @@ -3,6 +3,7 @@ import type { ChannelId, ChannelWebhookId } from "@hazel/schema" import { formatDistanceToNow } from "date-fns" import { useState } from "react" import { toast } from "sonner" +import { toDate } from "~/lib/utils" import { createChannelWebhookMutation, deleteChannelWebhookMutation, @@ -266,7 +267,7 @@ export function IntegrationCard({ provider, channelId, webhook, onWebhookChange {webhook.lastUsedAt && (

Last alert{" "} - {formatDistanceToNow(new Date(webhook.lastUsedAt), { addSuffix: true })} + {formatDistanceToNow(toDate(webhook.lastUsedAt), { addSuffix: true })}

)}
diff --git a/apps/web/src/components/channel-settings/openstatus-section.tsx b/apps/web/src/components/channel-settings/openstatus-section.tsx index e8d4f0487..0e4d770cc 100644 --- a/apps/web/src/components/channel-settings/openstatus-section.tsx +++ b/apps/web/src/components/channel-settings/openstatus-section.tsx @@ -3,6 +3,7 @@ import type { ChannelId, ChannelWebhookId } from "@hazel/schema" import { formatDistanceToNow } from "date-fns" import { Exit } from "effect" import { useState } from "react" +import { toDate } from "~/lib/utils" import { toast } from "sonner" import { createChannelWebhookMutation, @@ -272,7 +273,7 @@ export function OpenStatusSection({ {webhook.lastUsedAt && (

Last alert{" "} - {formatDistanceToNow(new Date(webhook.lastUsedAt), { addSuffix: true })} + {formatDistanceToNow(toDate(webhook.lastUsedAt), { addSuffix: true })}

)}
diff --git a/apps/web/src/components/channel-settings/railway-section.tsx b/apps/web/src/components/channel-settings/railway-section.tsx index fc88647ec..bd2664676 100644 --- a/apps/web/src/components/channel-settings/railway-section.tsx +++ b/apps/web/src/components/channel-settings/railway-section.tsx @@ -3,6 +3,7 @@ import type { ChannelId, ChannelWebhookId } from "@hazel/schema" import { formatDistanceToNow } from "date-fns" import { Exit } from "effect" import { useState } from "react" +import { toDate } from "~/lib/utils" import { toast } from "sonner" import { createChannelWebhookMutation, @@ -272,7 +273,7 @@ export function RailwaySection({ {webhook.lastUsedAt && (

Last alert{" "} - {formatDistanceToNow(new Date(webhook.lastUsedAt), { addSuffix: true })} + {formatDistanceToNow(toDate(webhook.lastUsedAt), { addSuffix: true })}

)}
diff --git a/apps/web/src/components/connect/share-channel-modal.test.tsx b/apps/web/src/components/connect/share-channel-modal.test.tsx index a52aa7eb2..a838bd892 100644 --- a/apps/web/src/components/connect/share-channel-modal.test.tsx +++ b/apps/web/src/components/connect/share-channel-modal.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { beforeEach, describe, expect, it, vi } from "vitest" -import type { OrganizationId } from "@hazel/schema" +import type { ChannelId, OrganizationId } from "@hazel/schema" import type { InputHTMLAttributes, ReactNode } from "react" const { workspaceSearchMutation, createConnectInviteMutation, searchWorkspacesMock, createInviteMock } = @@ -13,7 +13,7 @@ const { workspaceSearchMutation, createConnectInviteMutation, searchWorkspacesMo createInviteMock: vi.fn(), })) -vi.mock("@effect-atom/atom-react", () => ({ +vi.mock("@effect/atom-react", () => ({ useAtomSet: (mutation: symbol) => mutation === workspaceSearchMutation ? searchWorkspacesMock : createInviteMock, })) @@ -122,7 +122,9 @@ vi.mock("~/components/icons/icon-close", () => ({ import { ShareChannelModal } from "./share-channel-modal" -const ORG_ID = "00000000-0000-0000-0000-000000000101" as OrganizationId +const ORG_ID = "00000000-0000-4000-8000-000000000101" as OrganizationId +const GUEST_ORG_ID = "00000000-0000-4000-8000-000000000102" as OrganizationId +const CHANNEL_ID = "00000000-0000-4000-8000-000000000201" as ChannelId describe("ShareChannelModal", () => { beforeEach(() => { @@ -142,7 +144,7 @@ describe("ShareChannelModal", () => { logoUrl: null, }, { - id: "00000000-0000-0000-0000-000000000102" as OrganizationId, + id: GUEST_ORG_ID, name: "Guest Workspace", slug: "guest-workspace", logoUrl: null, @@ -156,7 +158,7 @@ describe("ShareChannelModal", () => { undefined} - channelId={"00000000-0000-0000-0000-000000000201" as any} + channelId={CHANNEL_ID} channelName="general" organizationId={ORG_ID} />, diff --git a/apps/web/src/components/integrations/openstatus-integration-content.tsx b/apps/web/src/components/integrations/openstatus-integration-content.tsx index 69b17434b..38700589d 100644 --- a/apps/web/src/components/integrations/openstatus-integration-content.tsx +++ b/apps/web/src/components/integrations/openstatus-integration-content.tsx @@ -3,6 +3,7 @@ import type { Channel } from "@hazel/domain/models" import type { ChannelId, OrganizationId } from "@hazel/schema" import { eq, or, useLiveQuery } from "@tanstack/react-db" import { formatDistanceToNow } from "date-fns" +import { toDate } from "~/lib/utils" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { toast } from "sonner" import { listOrganizationWebhooksMutation, type WebhookData } from "~/atoms/channel-webhook-atoms" @@ -286,7 +287,7 @@ export function OpenStatusIntegrationContent({ organizationId }: OpenStatusInteg

{webhook.lastUsedAt - ? `Last alert ${formatDistanceToNow(new Date(webhook.lastUsedAt), { addSuffix: true })}` + ? `Last alert ${formatDistanceToNow(toDate(webhook.lastUsedAt), { addSuffix: true })}` : "No alerts received yet"}

diff --git a/apps/web/src/components/integrations/railway-integration-content.tsx b/apps/web/src/components/integrations/railway-integration-content.tsx index 051de40af..a4a78f3be 100644 --- a/apps/web/src/components/integrations/railway-integration-content.tsx +++ b/apps/web/src/components/integrations/railway-integration-content.tsx @@ -3,6 +3,7 @@ import type { Channel } from "@hazel/domain/models" import type { ChannelId, OrganizationId } from "@hazel/schema" import { eq, or, useLiveQuery } from "@tanstack/react-db" import { formatDistanceToNow } from "date-fns" +import { toDate } from "~/lib/utils" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { toast } from "sonner" import { listOrganizationWebhooksMutation, type WebhookData } from "~/atoms/channel-webhook-atoms" @@ -285,7 +286,7 @@ export function RailwayIntegrationContent({ organizationId }: RailwayIntegration

{webhook.lastUsedAt - ? `Last alert ${formatDistanceToNow(new Date(webhook.lastUsedAt), { addSuffix: true })}` + ? `Last alert ${formatDistanceToNow(toDate(webhook.lastUsedAt), { addSuffix: true })}` : "No alerts received yet"}

diff --git a/apps/web/src/components/integrations/rss-subscriptions-section.tsx b/apps/web/src/components/integrations/rss-subscriptions-section.tsx index 2a2d44520..0ae48da0e 100644 --- a/apps/web/src/components/integrations/rss-subscriptions-section.tsx +++ b/apps/web/src/components/integrations/rss-subscriptions-section.tsx @@ -3,6 +3,7 @@ import type { Channel } from "@hazel/domain/models" import type { OrganizationId, RssSubscriptionId } from "@hazel/schema" import { eq, or, useLiveQuery } from "@tanstack/react-db" import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { toDate } from "~/lib/utils" import { deleteRssSubscriptionMutation, listOrganizationRssSubscriptionsMutation, @@ -367,7 +368,7 @@ function SubscriptionRow({ subscription, channelName, onToggle, onDelete }: Subs <> · - Last fetched {new Date(subscription.lastFetchedAt).toLocaleDateString()} + Last fetched {toDate(subscription.lastFetchedAt).toLocaleDateString()} )} diff --git a/apps/web/src/components/link-preview.tsx b/apps/web/src/components/link-preview.tsx index f6a2d616b..a40d20cd8 100644 --- a/apps/web/src/components/link-preview.tsx +++ b/apps/web/src/components/link-preview.tsx @@ -6,7 +6,7 @@ import { useMemo } from "react" import { LinkPreviewClient } from "~/lib/services/common/link-preview-client" export function LinkPreview({ url }: { url: string }) { - const previewResult = useAtomValue(LinkPreviewClient.query("linkPreview", "get", { query: { url } })) + const previewResult = useAtomValue(LinkPreviewClient.query("linkPreview", "get", { payload: { url } })) const og = AsyncResult.getOrElse(previewResult, () => null) const isLoading = AsyncResult.isInitial(previewResult) diff --git a/apps/web/src/components/modals/create-organization-modal.tsx b/apps/web/src/components/modals/create-organization-modal.tsx index fb84f8c0d..35de99aa5 100644 --- a/apps/web/src/components/modals/create-organization-modal.tsx +++ b/apps/web/src/components/modals/create-organization-modal.tsx @@ -65,9 +65,9 @@ export function CreateOrganizationModal({ isOpen, onOpenChange }: CreateOrganiza login({ organizationId: result.data.id, returnTo: returnUrl }) }) .successMessage("Server created successfully") - .onErrorTag("OrganizationSlugAlreadyExistsError", (error) => ({ + .onErrorTag("OrganizationSlugAlreadyExistsError", () => ({ title: "Slug already taken", - description: `The slug "${error.slug}" is already in use. Please choose a different one.`, + description: "That workspace URL is already in use. Please choose a different one.", isRetryable: false, })) .run() diff --git a/apps/web/src/components/notifications/notification-item.tsx b/apps/web/src/components/notifications/notification-item.tsx index 4fc4f0feb..e461c2ccd 100644 --- a/apps/web/src/components/notifications/notification-item.tsx +++ b/apps/web/src/components/notifications/notification-item.tsx @@ -1,6 +1,7 @@ import type { NotificationId } from "@hazel/schema" import { Link, useParams } from "@tanstack/react-router" import { formatDistanceToNow } from "date-fns" +import { toDate } from "~/lib/utils" import IconHashtag from "~/components/icons/icon-hashtag" import IconMsgs from "~/components/icons/icon-msgs" import { Avatar } from "~/components/ui/avatar" @@ -118,7 +119,7 @@ export function NotificationItem({ notification, onMarkAsRead }: NotificationIte {getMessagePreview(notification)}

- {formatDistanceToNow(new Date(n.createdAt), { addSuffix: true })} + {formatDistanceToNow(toDate(n.createdAt), { addSuffix: true })}

{getNotificationContext(notification)}

diff --git a/apps/web/src/components/onboarding/org-setup-step.tsx b/apps/web/src/components/onboarding/org-setup-step.tsx index e007de294..984f4a077 100644 --- a/apps/web/src/components/onboarding/org-setup-step.tsx +++ b/apps/web/src/components/onboarding/org-setup-step.tsx @@ -74,12 +74,12 @@ export function OrgSetupStep({ isPublic: false, }, }) - exitToast(exit) - .onErrorTag("OrganizationSlugAlreadyExistsError", (error) => ({ - title: "Slug already taken", - description: `The slug "${error.slug}" is already in use. Please choose a different one.`, - isRetryable: false, - })) + exitToast(exit) + .onErrorTag("OrganizationSlugAlreadyExistsError", () => ({ + title: "Slug already taken", + description: "That workspace URL is already in use. Please choose a different one.", + isRetryable: false, + })) .run() if (Exit.isSuccess(exit)) { diff --git a/apps/web/src/components/sidebar/channel-item.tsx b/apps/web/src/components/sidebar/channel-item.tsx index 3fa591839..f78dd15af 100644 --- a/apps/web/src/components/sidebar/channel-item.tsx +++ b/apps/web/src/components/sidebar/channel-item.tsx @@ -1,5 +1,6 @@ import { useAtomSet } from "@effect/atom-react" -import type { ChannelSectionId } from "@hazel/schema" +import type { ChannelId, ChannelMemberId, ChannelSectionId, OrganizationId, UserId } from "@hazel/schema" +import type { DateTime } from "effect" import { useNavigate } from "@tanstack/react-router" import { memo } from "react" import { useModal } from "~/atoms/modal-atoms" @@ -24,18 +25,38 @@ import { usePermission } from "~/hooks/use-permission" import { useScrollIntoViewOnActive } from "~/hooks/use-scroll-into-view-on-active" import { exitToastAsync } from "~/lib/toast-exit" -import type { channelCollection, channelMemberCollection } from "~/db/collections" +type DateLike = Date | DateTime.Utc -type ChannelData = (typeof channelCollection)["_type"] -type ChannelMemberData = (typeof channelMemberCollection)["_type"] +export interface SidebarChannelData { + readonly id: ChannelId + readonly name: string + readonly type: "private" | "public" | "thread" | "direct" | "single" + readonly icon: string | null + readonly organizationId: OrganizationId + readonly parentChannelId: ChannelId | null + readonly sectionId: ChannelSectionId | null + readonly updatedAt?: DateLike | null + readonly [key: string]: unknown +} + +export interface SidebarChannelMemberData { + readonly id: ChannelMemberId + readonly channelId: ChannelId + readonly userId: UserId + readonly isMuted: boolean + readonly isFavorite: boolean + readonly isHidden: boolean + readonly notificationCount: number + readonly [key: string]: unknown +} interface ChannelItemProps { - channel: ChannelData - member: ChannelMemberData + channel: SidebarChannelData + member: SidebarChannelMemberData notificationCount?: number threads?: Array<{ - channel: ChannelData - member: ChannelMemberData + channel: SidebarChannelData + member: SidebarChannelMemberData }> /** Available sections for "move to section" menu */ sections?: Array<{ id: ChannelSectionId; name: string }> diff --git a/apps/web/src/components/sidebar/channels-sidebar.tsx b/apps/web/src/components/sidebar/channels-sidebar.tsx index 3b46bd60d..f2fa8601c 100644 --- a/apps/web/src/components/sidebar/channels-sidebar.tsx +++ b/apps/web/src/components/sidebar/channels-sidebar.tsx @@ -60,6 +60,8 @@ import IconClose from "../icons/icon-close" import IconLightbulb from "../icons/icon-lightbulb" import { Button } from "../ui/button" +import type { SidebarChannelData, SidebarChannelMemberData } from "~/components/sidebar/channel-item" + interface ChannelSectionProps { organizationId: OrganizationId sectionId: ChannelSectionId | null @@ -72,8 +74,8 @@ interface ChannelSectionProps { threadsByParent?: Map< string, Array<{ - channel: Omit & { updatedAt: Date | null } - member: import("@hazel/db/schema").ChannelMember + channel: SidebarChannelData + member: SidebarChannelMemberData }> > /** Available sections for "move to section" menu */ diff --git a/apps/web/src/components/sidebar/thread-item.tsx b/apps/web/src/components/sidebar/thread-item.tsx index 15df30c0a..334035da7 100644 --- a/apps/web/src/components/sidebar/thread-item.tsx +++ b/apps/web/src/components/sidebar/thread-item.tsx @@ -1,6 +1,5 @@ import { useAtomSet } from "@effect/atom-react" -import type { Channel, ChannelMember } from "@hazel/db/schema" -import type { ChannelId } from "@hazel/schema" +import type { ChannelId, ChannelMemberId } from "@hazel/schema" import { Exit } from "effect" import { useState } from "react" import { toast } from "sonner" @@ -21,8 +20,16 @@ import { useOrganization } from "~/hooks/use-organization" import { useScrollIntoViewOnActive } from "~/hooks/use-scroll-into-view-on-active" interface ThreadItemProps { - thread: Omit & { updatedAt: Date | null } - member: ChannelMember + thread: { + readonly id: ChannelId + readonly name: string + } + member: { + readonly id: ChannelMemberId + readonly isMuted: boolean + readonly isFavorite: boolean + readonly isHidden: boolean + } } export function ThreadItem({ thread, member }: ThreadItemProps) { diff --git a/apps/web/src/components/theme-provider.tsx b/apps/web/src/components/theme-provider.tsx index 37b4fc368..c6342ac73 100644 --- a/apps/web/src/components/theme-provider.tsx +++ b/apps/web/src/components/theme-provider.tsx @@ -14,14 +14,13 @@ type ThemeProviderProps = { storageKey?: string } -const ThemeSchema = Schema.Literal("dark", "light", "system") +const ThemeSchema = Schema.Literals(["dark", "light", "system"]) const HexColorSchema = Schema.String.pipe( - Schema.pattern(/^#[0-9A-Fa-f]{6}$/), - Schema.annotations({ message: () => "Must be a valid hex color (#RRGGBB)" }), + Schema.check(Schema.isPattern(/^#[0-9A-Fa-f]{6}$/)), ) -const GrayPaletteSchema = Schema.Literal( +const GrayPaletteSchema = Schema.Literals([ "gray", "gray-blue", "gray-cool", @@ -30,9 +29,9 @@ const GrayPaletteSchema = Schema.Literal( "gray-iron", "gray-true", "gray-warm", -) +]) -const RadiusPresetSchema = Schema.Literal("tight", "normal", "round", "full") +const RadiusPresetSchema = Schema.Literals(["tight", "normal", "round", "full"]) const ThemeCustomizationSchema = Schema.Struct({ primary: HexColorSchema, @@ -40,28 +39,30 @@ const ThemeCustomizationSchema = Schema.Struct({ radius: RadiusPresetSchema, }) +type ThemeCustomization = typeof ThemeCustomizationSchema.Type + // Theme mode atom with automatic localStorage persistence export const themeAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "hazel-ui-theme", - schema: Schema.toCodecIso(Schema.NullOr(ThemeSchema)), - defaultValue: () => "system" as const, + schema: Schema.toCodecJson(Schema.NullOr(ThemeSchema)), + defaultValue: () => "system" as Theme | null, }) // Brand color atom (legacy, kept for backward compatibility) export const brandColorAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "brand-color", - schema: Schema.toCodecIso(Schema.NullOr(HexColorSchema)), - defaultValue: () => DEFAULT_BRAND_COLOR as string, + schema: Schema.toCodecJson(Schema.NullOr(HexColorSchema)), + defaultValue: () => DEFAULT_BRAND_COLOR as string | null, }) // Full theme customization atom export const themeCustomizationAtom = Atom.kvs({ runtime: platformStorageRuntime, key: "hazel-theme-customization", - schema: Schema.toCodecIso(Schema.NullOr(ThemeCustomizationSchema)), - defaultValue: () => getDefaultThemeCustomization() as ThemeModel.ThemeCustomization, + schema: Schema.toCodecJson(Schema.NullOr(ThemeCustomizationSchema)), + defaultValue: () => getDefaultThemeCustomization() as ThemeCustomization | null, }) // Resolved theme (system -> light/dark) @@ -69,7 +70,7 @@ export const resolvedThemeAtom = Atom.transform(themeAtom, (get) => { const theme = get(themeAtom) if (theme !== "system" && theme !== null) return theme - if (typeof window === "undefined") return "light" + if (typeof window === "undefined") return "light" as const // Listen to system theme changes const matcher = window.matchMedia("(prefers-color-scheme: dark)") @@ -77,10 +78,10 @@ export const resolvedThemeAtom = Atom.transform(themeAtom, (get) => { matcher.addEventListener("change", onChange) get.addFinalizer(() => matcher.removeEventListener("change", onChange)) - return matcher.matches ? "dark" : "light" + return matcher.matches ? ("dark" as const) : ("light" as const) function onChange() { - get.setSelf(matcher.matches ? "dark" : "light") + get.setSelf(matcher.matches ? ("dark" as const) : ("light" as const)) } }) @@ -115,7 +116,7 @@ export const applyThemeCustomizationAtom = Atom.make((get) => { const isDarkMode = resolvedTheme === "dark" // Apply all customization parts - applyBrandColor(customization.primary as ThemeModel.HexColor) + applyBrandColor(customization.primary as unknown as ThemeModel.HexColor) applyGrayPalette(customization.grayPalette as ThemeModel.GrayPalette, isDarkMode) applyRadius(customization.radius as ThemeModel.RadiusPreset) }) @@ -132,7 +133,7 @@ export const applyBrandColorAtom = Atom.make((get) => { if (typeof document === "undefined") return const hexColor = brandColor || DEFAULT_BRAND_COLOR - applyBrandColor(hexColor as ThemeModel.HexColor) + applyBrandColor(hexColor as unknown as ThemeModel.HexColor) }) export function ThemeProvider({ children }: ThemeProviderProps) { @@ -157,19 +158,19 @@ export const useTheme = () => { // When setting brand color, also update the customization const handleSetBrandColor = (color: string) => { - setBrandColor(color) + setBrandColor(color as string & null) if (customization) { setCustomization({ ...customization, primary: color, - }) + } as unknown as ThemeCustomization) } } return { - theme: theme || "system", - setTheme, - brandColor: customization?.primary || brandColor || DEFAULT_BRAND_COLOR, + theme: (theme || "system") as Theme, + setTheme: setTheme as (value: Theme | null) => void, + brandColor: (customization?.primary || brandColor || DEFAULT_BRAND_COLOR) as string, setBrandColor: handleSetBrandColor, } } @@ -188,29 +189,29 @@ export const useThemeCustomization = () => { setCustomization({ ...activeCustomization, primary: color, - }) + } as unknown as ThemeCustomization) } const setGrayPalette = (palette: ThemeModel.GrayPalette) => { setCustomization({ ...activeCustomization, grayPalette: palette, - }) + } as unknown as ThemeCustomization) } const setRadius = (radius: ThemeModel.RadiusPreset) => { setCustomization({ ...activeCustomization, radius: radius, - }) + } as unknown as ThemeCustomization) } const setFullCustomization = (newCustomization: ThemeModel.ThemeCustomization) => { - setCustomization(newCustomization) + setCustomization(newCustomization as unknown as ThemeCustomization) } return { - customization: activeCustomization, + customization: activeCustomization as ThemeModel.ThemeCustomization, isDarkMode: resolvedTheme === "dark", setPrimary, setGrayPalette, diff --git a/apps/web/src/components/tweet-embed.tsx b/apps/web/src/components/tweet-embed.tsx index 14981c5b7..567564bbb 100644 --- a/apps/web/src/components/tweet-embed.tsx +++ b/apps/web/src/components/tweet-embed.tsx @@ -237,7 +237,7 @@ interface TweetEmbedProps { } export function TweetEmbed({ id, author, messageCreatedAt }: TweetEmbedProps) { - const tweetResult = useAtomValue(LinkPreviewClient.query("tweet", "get", { query: { id } })) + const tweetResult = useAtomValue(LinkPreviewClient.query("tweet", "get", { payload: { id } })) const tweet = AsyncResult.getOrElse(tweetResult, () => null) const isLoading = AsyncResult.isInitial(tweetResult) diff --git a/apps/web/src/db/actions.ts b/apps/web/src/db/actions.ts index ad0f7fe3d..a46602f61 100644 --- a/apps/web/src/db/actions.ts +++ b/apps/web/src/db/actions.ts @@ -54,7 +54,7 @@ const getMountedConversationId = (channelId: ChannelId) => */ const MessageRetrySchedule = Schedule.exponential(Duration.seconds(1), 2).pipe( Schedule.jittered, - Schedule.whileInput(isErrorRetryable), + Schedule.while((metadata) => isErrorRetryable(metadata.input)), Schedule.both(Schedule.recurs(3)), ) diff --git a/apps/web/src/hooks/use-grouped-notifications.ts b/apps/web/src/hooks/use-grouped-notifications.ts index 1711dc75b..d1969f5ba 100644 --- a/apps/web/src/hooks/use-grouped-notifications.ts +++ b/apps/web/src/hooks/use-grouped-notifications.ts @@ -1,5 +1,6 @@ import { isThisWeek, isToday, isYesterday } from "date-fns" import { useMemo } from "react" +import { toDate } from "~/lib/utils" import type { NotificationWithDetails } from "./use-notifications" import { useNotifications } from "./use-notifications" @@ -20,7 +21,7 @@ function groupNotificationsByTime(notifications: NotificationWithDetails[]): Not const older: NotificationWithDetails[] = [] for (const notification of notifications) { - const createdAt = new Date(notification.notification.createdAt) + const createdAt = toDate(notification.notification.createdAt) if (isToday(createdAt)) { today.push(notification) diff --git a/apps/web/src/hooks/use-typing.test.tsx b/apps/web/src/hooks/use-typing.test.tsx index e0abc47f1..4a075d0d6 100644 --- a/apps/web/src/hooks/use-typing.test.tsx +++ b/apps/web/src/hooks/use-typing.test.tsx @@ -1,4 +1,5 @@ import { renderHook, act } from "@testing-library/react" +import type { ChannelId, ChannelMemberId } from "@hazel/schema" import { Exit } from "effect" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { useTyping } from "./use-typing" @@ -19,7 +20,7 @@ vi.mock("~/lib/typing-diagnostics", () => ({ pushTypingDiagnostics: vi.fn(), })) -vi.mock("@effect-atom/atom-react", () => ({ +vi.mock("@effect/atom-react", () => ({ useAtomSet: vi.fn((mutation: unknown) => { if (mutation === upsertMutationSymbol) return upsertMock if (mutation === deleteMutationSymbol) return deleteMock @@ -34,6 +35,9 @@ const flushMicrotasks = async () => { } describe("useTyping", () => { + const channelId = "00000000-0000-4000-8000-000000000301" as ChannelId + const memberId = "00000000-0000-4000-8000-000000000302" as ChannelMemberId + beforeEach(() => { vi.useFakeTimers() vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z")) @@ -43,13 +47,13 @@ describe("useTyping", () => { Exit.succeed({ data: { id: "typing-indicator-1" }, transactionId: 1, - }) as any, + }), ) deleteMock.mockResolvedValue( Exit.succeed({ data: { id: "typing-indicator-1" }, transactionId: 1, - }) as any, + }), ) }) @@ -60,8 +64,8 @@ describe("useTyping", () => { it("sends heartbeat immediately on first non-empty content and throttles subsequent heartbeats", async () => { const { result } = renderHook(() => useTyping({ - channelId: "channel-1" as any, - memberId: "member-1" as any, + channelId, + memberId, heartbeatInterval: 1500, }), ) @@ -91,8 +95,8 @@ describe("useTyping", () => { it("deletes typing indicator when content becomes empty", async () => { const { result } = renderHook(() => useTyping({ - channelId: "channel-1" as any, - memberId: "member-1" as any, + channelId, + memberId, }), ) @@ -117,8 +121,8 @@ describe("useTyping", () => { it("auto-stops after typing timeout and deletes indicator", async () => { const { result } = renderHook(() => useTyping({ - channelId: "channel-1" as any, - memberId: "member-1" as any, + channelId, + memberId, typingTimeout: 3000, }), ) @@ -140,15 +144,15 @@ describe("useTyping", () => { it("cleans up previous indicator when channel/member context changes", async () => { const { result, rerender } = renderHook( - (props: { channelId: string; memberId: string }) => + (props: { channelId: ChannelId; memberId: ChannelMemberId }) => useTyping({ - channelId: props.channelId as any, - memberId: props.memberId as any, + channelId: props.channelId, + memberId: props.memberId, }), { initialProps: { - channelId: "channel-1", - memberId: "member-1", + channelId, + memberId, }, }, ) @@ -159,8 +163,8 @@ describe("useTyping", () => { await flushMicrotasks() rerender({ - channelId: "channel-2", - memberId: "member-2", + channelId: "00000000-0000-4000-8000-000000000303" as ChannelId, + memberId: "00000000-0000-4000-8000-000000000304" as ChannelMemberId, }) await flushMicrotasks() diff --git a/apps/web/src/lib/auth-fetch.ts b/apps/web/src/lib/auth-fetch.ts index 31ef13db8..f34564461 100644 --- a/apps/web/src/lib/auth-fetch.ts +++ b/apps/web/src/lib/auth-fetch.ts @@ -23,8 +23,14 @@ const WebTokenStorageLive = WebTokenStorage.layer */ const clearTokens = async (): Promise => { const effect = isTauri() - ? TokenStorage.clearTokens.pipe(Effect.provide(DesktopTokenStorageLive)) - : WebTokenStorage.clearTokens.pipe(Effect.provide(WebTokenStorageLive)) + ? Effect.gen(function* () { + const storage = yield* TokenStorage + yield* storage.clearTokens + }).pipe(Effect.provide(DesktopTokenStorageLive)) + : Effect.gen(function* () { + const storage = yield* WebTokenStorage + yield* storage.clearTokens + }).pipe(Effect.provide(WebTokenStorageLive)) return runtime.runPromise( effect.pipe( Effect.catch(() => Effect.void), diff --git a/apps/web/src/lib/auth-token.ts b/apps/web/src/lib/auth-token.ts index 592941846..36f83d37f 100644 --- a/apps/web/src/lib/auth-token.ts +++ b/apps/web/src/lib/auth-token.ts @@ -5,7 +5,7 @@ * React/async consumers use the exported Promise wrappers. */ -import { Deferred, Duration, Effect, Option, Ref } from "effect" +import { Deferred, Duration, Effect, Option, Ref, type ServiceMap } from "effect" import { appRegistry } from "~/lib/registry" import { webTokensAtom, webAuthErrorAtom } from "~/atoms/web-auth" import { desktopTokensAtom, desktopAuthErrorAtom } from "~/atoms/desktop-auth" @@ -37,15 +37,25 @@ const webStorageLive = WebTokenStorage.layer const desktopStorageLive = TokenStorage.layer const tokenExchangeLive = TokenExchange.layer +type TokenExchangeService = ServiceMap.Service.Shape +type WebTokenStorageService = ServiceMap.Service.Shape +type DesktopTokenStorageService = ServiceMap.Service.Shape + // ============================================================================ // Error Classification // ============================================================================ +interface ErrorLike { + _tag?: string + message?: string + detail?: string +} + /** * Check if an error is a fatal error (refresh token revoked/invalid) * Fatal errors should not be retried */ -export const isFatalRefreshError = (error: { _tag?: string; message?: string; detail?: string }): boolean => { +export const isFatalRefreshError = (error: ErrorLike): boolean => { if (error.detail?.includes("HTTP 401")) return true if (error.detail?.includes("HTTP 403")) return true return false @@ -54,7 +64,7 @@ export const isFatalRefreshError = (error: { _tag?: string; message?: string; de /** * Check if an error is transient (timeout, network) and can be retried */ -export const isTransientError = (error: { _tag?: string; message?: string }): boolean => { +export const isTransientError = (error: ErrorLike): boolean => { const message = error.message?.toLowerCase() ?? "" return ( message.includes("timed out") || @@ -74,38 +84,49 @@ const errorAtom = () => (isTauri() ? desktopAuthErrorAtom : webAuthErrorAtom) const platformTag = () => (isTauri() ? "desktop" : "web") /** Read access token from the correct platform storage */ -const readAccessToken = Effect.fn("readAccessToken")(function* () { - if (isTauri()) { - const storage = yield* TokenStorage - return Option.getOrNull(yield* storage.getAccessToken) - } - const storage = yield* WebTokenStorage +const readAccessTokenDesktop = Effect.gen(function* () { + const storage: DesktopTokenStorageService = yield* TokenStorage + return Option.getOrNull(yield* storage.getAccessToken) +}).pipe(Effect.provide(desktopStorageLive)) + +const readAccessTokenWeb = Effect.gen(function* () { + const storage: WebTokenStorageService = yield* WebTokenStorage return Option.getOrNull(yield* storage.getAccessToken) -}).pipe(Effect.provide(isTauri() ? desktopStorageLive : webStorageLive)) as Effect.Effect +}).pipe(Effect.provide(webStorageLive)) + +const readAccessToken = Effect.suspend(() => (isTauri() ? readAccessTokenDesktop : readAccessTokenWeb)).pipe( + Effect.catch(() => Effect.succeed(null)), +) /** Read refresh token from the correct platform storage */ -const readRefreshToken = Effect.fn("readRefreshToken")(function* () { - if (isTauri()) { - const storage = yield* TokenStorage - return yield* storage.getRefreshToken - } - const storage = yield* WebTokenStorage +const readRefreshTokenDesktop = Effect.gen(function* () { + const storage: DesktopTokenStorageService = yield* TokenStorage return yield* storage.getRefreshToken -}).pipe(Effect.provide(isTauri() ? desktopStorageLive : webStorageLive)) as Effect.Effect< - Option.Option -> +}).pipe(Effect.provide(desktopStorageLive)) + +const readRefreshTokenWeb = Effect.gen(function* () { + const storage: WebTokenStorageService = yield* WebTokenStorage + return yield* storage.getRefreshToken +}).pipe(Effect.provide(webStorageLive)) + +const readRefreshToken = Effect.suspend(() => + isTauri() ? readRefreshTokenDesktop : readRefreshTokenWeb, +).pipe(Effect.catch(() => Effect.succeed(Option.none()))) /** Store tokens in the correct platform storage */ const storeTokens = (accessToken: string, refreshToken: string, expiresIn: number) => - Effect.gen(function* () { + Effect.suspend(() => { if (isTauri()) { - const storage = yield* TokenStorage - yield* storage.storeTokens(accessToken, refreshToken, expiresIn) - } else { - const storage = yield* WebTokenStorage - yield* storage.storeTokens(accessToken, refreshToken, expiresIn) + return Effect.gen(function* () { + const storage: DesktopTokenStorageService = yield* TokenStorage + yield* storage.storeTokens(accessToken, refreshToken, expiresIn) + }).pipe(Effect.provide(desktopStorageLive)) } - }).pipe(Effect.provide(isTauri() ? desktopStorageLive : webStorageLive), Effect.orDie) + return Effect.gen(function* () { + const storage: WebTokenStorageService = yield* WebTokenStorage + yield* storage.storeTokens(accessToken, refreshToken, expiresIn) + }).pipe(Effect.provide(webStorageLive)) + }).pipe(Effect.orDie) // ============================================================================ // Core Effects @@ -115,10 +136,7 @@ const storeTokens = (accessToken: string, refreshToken: string, expiresIn: numbe * Get the current access token from the appropriate platform storage. * Returns null if not authenticated. */ -const getAccessTokenEffect: Effect.Effect = readAccessToken.pipe( - Effect.catch(() => Effect.succeed(null)), - Effect.withSpan("getAccessToken"), -) +const getAccessTokenEffect = readAccessToken.pipe(Effect.withSpan("getAccessToken")) /** * Wait for any in-progress token refresh to complete. @@ -172,13 +190,18 @@ const forceRefreshEffect: Effect.Effect = Effect.gen(function* () { const resultRef = yield* Ref.make(false) + type RefreshTokens = { accessToken: string; refreshToken: string; expiresIn: number } + type RefreshResult = { success: true; tokens: RefreshTokens } | { success: false; error: ErrorLike } + const attemptRefresh = (attempt: number): Effect.Effect => Effect.gen(function* () { - const tokenExchange = yield* TokenExchange + const tokenExchange: TokenExchangeService = yield* TokenExchange - const refreshResult = yield* tokenExchange.refreshToken(refreshTokenOpt.value).pipe( - Effect.map((tokens) => ({ success: true as const, tokens })), - Effect.catch((error) => Effect.succeed({ success: false as const, error })), + const refreshResult: RefreshResult = yield* tokenExchange.refreshToken(refreshTokenOpt.value).pipe( + Effect.map((tokens): RefreshResult => ({ success: true, tokens })), + Effect.catch((error): Effect.Effect => + Effect.succeed({ success: false, error: error as ErrorLike }), + ), ) if (refreshResult.success) { diff --git a/apps/web/src/lib/connect-shared-channels.ts b/apps/web/src/lib/connect-shared-channels.ts index 23f0e09f7..10d484ed5 100644 --- a/apps/web/src/lib/connect-shared-channels.ts +++ b/apps/web/src/lib/connect-shared-channels.ts @@ -1,10 +1,11 @@ import type { ChannelId, ConnectConversationId } from "@hazel/schema" +import type { DateTime } from "effect" type ConnectMountLike = { channelId: ChannelId conversationId: ConnectConversationId isActive: boolean - deletedAt: Date | null + deletedAt: Date | DateTime.Utc | null } const isActiveMount = (mount: ConnectMountLike) => mount.isActive && mount.deletedAt === null diff --git a/apps/web/src/lib/electric-fetch.ts b/apps/web/src/lib/electric-fetch.ts index 5860cfe64..cdea28f7b 100644 --- a/apps/web/src/lib/electric-fetch.ts +++ b/apps/web/src/lib/electric-fetch.ts @@ -33,7 +33,7 @@ const hasAuthToken = (): boolean => { const retrySchedule = Schedule.exponential("2 seconds").pipe( Schedule.jittered, Schedule.either(Schedule.spaced("60 seconds")), - Schedule.upTo(8), + Schedule.compose(Schedule.recurs(8)), ) /** @@ -43,7 +43,7 @@ const shouldRetry = (response: Response): boolean => response.status >= 500 && r /** * Electric fetch client with exponential backoff retry for server errors. - * - Retries 5xx errors with exponential backoff (2s → 4s → 8s... up to 60s) + * - Retries 5xx errors with exponential backoff (2s -> 4s -> 8s... up to 60s) * - Does NOT retry 4xx client errors (auth, validation, etc.) * - Uses jitter to prevent thundering herd */ @@ -79,5 +79,5 @@ export const electricFetchClient = async ( Effect.catch((error) => (error instanceof Response ? Effect.succeed(error) : Effect.fail(error))), ) - return runtime.runPromise(withRetry) + return runtime.runPromise(withRetry) as Promise } diff --git a/apps/web/src/lib/error-messages.ts b/apps/web/src/lib/error-messages.ts index c5e07598c..19635e117 100644 --- a/apps/web/src/lib/error-messages.ts +++ b/apps/web/src/lib/error-messages.ts @@ -1,4 +1,4 @@ -import type { HttpClientError } from "effect/unstable/http" +import { HttpClientError } from "effect/unstable/http" import { RpcClientError } from "effect/unstable/rpc" import { AIProviderUnavailableError, @@ -32,7 +32,7 @@ import { WorkflowServiceUnavailableError, WorkOSUserFetchError, } from "@hazel/domain/errors" -import { Cause, Chunk, Match, Option, Schema } from "effect" +import { Cause, Match, Schema } from "effect" import { CollectionInErrorEffectError, CollectionSyncEffectError, @@ -79,7 +79,6 @@ export const CommonAppErrorSchema = Schema.Union([ // Infrastructure errors (appear in most RPC calls) OptimisticActionError, SyncError, - RpcClientError, // TanStack DB errors (permanent - non-retryable) DuplicateKeyEffectError, KeyUpdateNotAllowedEffectError, @@ -124,7 +123,7 @@ export type CommonAppError = | typeof CommonAppErrorSchema.Type // Non-Schema errors (still have _tag but not Schema.TaggedError) | Schema.SchemaError - | HttpClientError + | HttpClientError.HttpClientError /** * Static error messages for errors that don't need dynamic content @@ -449,7 +448,7 @@ const isSchemaCommonError = Schema.is(CommonAppErrorSchema) /** * Tags for non-Schema errors that are still common */ -const NON_SCHEMA_COMMON_TAGS = new Set(["ParseError", "HttpClientError", "HttpClientError"]) +const NON_SCHEMA_COMMON_TAGS = new Set(["ParseError", "HttpClientError", "RpcClientError"]) /** * Type guard for CommonAppError. @@ -502,11 +501,10 @@ function isTimeoutError(error: unknown): boolean { * Uses type-safe Match for common errors, falls back to message extraction. */ export function getUserFriendlyError(cause: Cause.Cause): UserErrorMessage { - const failures = Cause.failures(cause) - const firstFailureOption = Chunk.head(failures) + const firstFailure = cause.reasons.find(Cause.isFailReason)?.error - if (Option.isSome(firstFailureOption)) { - const error = firstFailureOption.value + if (firstFailure !== undefined) { + const error = firstFailure // Check for network errors first if (isNetworkError(error)) { @@ -541,11 +539,10 @@ export function getUserFriendlyError(cause: Cause.Cause): UserErrorMessage } // Check defects (unexpected errors) - const defects = Cause.defects(cause) - const firstDefectOption = Chunk.head(defects) + const firstDefect = cause.reasons.find(Cause.isDieReason)?.defect - if (Option.isSome(firstDefectOption)) { - const defect = firstDefectOption.value + if (firstDefect !== undefined) { + const defect = firstDefect if (defect instanceof Error) { return { title: defect.message, isRetryable: false } } diff --git a/apps/web/src/lib/platform-storage/tauri-key-value-store.ts b/apps/web/src/lib/platform-storage/tauri-key-value-store.ts index f89cd993d..25ba4bf24 100644 --- a/apps/web/src/lib/platform-storage/tauri-key-value-store.ts +++ b/apps/web/src/lib/platform-storage/tauri-key-value-store.ts @@ -10,7 +10,7 @@ import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore" import { KeyValueStoreError as SystemError } from "effect/unstable/persistence/KeyValueStore" import { getTauriStore, type TauriStoreApi } from "@hazel/desktop/bridge" -import { Effect, Layer, Option } from "effect" +import { Effect, Layer } from "effect" const STORE_NAME = "settings.json" @@ -25,11 +25,10 @@ const loadStore: Effect.Effect = Effect.tryPromise({ }, catch: (error) => new SystemError({ - reason: "Unknown", - module: "KeyValueStore", + message: `Failed to load Tauri store: ${error}`, method: "getStore", - pathOrDescriptor: STORE_NAME, - description: `Failed to load Tauri store: ${error}`, + key: STORE_NAME, + cause: error, }), }) @@ -38,11 +37,10 @@ const getStore = Effect.cached(loadStore) const makeError = (method: string, key: string, error: unknown) => new SystemError({ - reason: "Unknown", - module: "KeyValueStore", + message: `Tauri store ${method} failed: ${error}`, method, - pathOrDescriptor: key, - description: `Tauri store ${method} failed: ${error}`, + key, + cause: error, }) /** @@ -57,10 +55,7 @@ export const layerTauriStore: Layer.Layer = Layer.e return KeyValueStore.makeStringOnly({ get: (key: string) => Effect.tryPromise({ - try: async () => { - const value = await store.get(key) - return Option.fromNullishOr(value) - }, + try: async () => await store.get(key), catch: (error) => makeError("get", key, error), }), diff --git a/apps/web/src/lib/rpc-auth-middleware.ts b/apps/web/src/lib/rpc-auth-middleware.ts index 8f2ea68a1..12da4f46c 100644 --- a/apps/web/src/lib/rpc-auth-middleware.ts +++ b/apps/web/src/lib/rpc-auth-middleware.ts @@ -10,7 +10,7 @@ import { AuthMiddleware } from "@hazel/domain/rpc" import { Effect } from "effect" import { waitForRefreshEffect, getAccessTokenEffect } from "~/lib/auth-token" -export const AuthMiddlewareClientLive = RpcMiddleware.layerClient(AuthMiddleware, ({ request }) => +export const AuthMiddlewareClientLive = RpcMiddleware.layerClient(AuthMiddleware, ({ request, next }) => Effect.gen(function* () { yield* waitForRefreshEffect @@ -18,9 +18,9 @@ export const AuthMiddlewareClientLive = RpcMiddleware.layerClient(AuthMiddleware if (token) { const newHeaders = Headers.set(request.headers, "authorization", `Bearer ${token}`) - return { ...request, headers: newHeaders } + return yield* next({ ...request, headers: newHeaders }) } - return request + return yield* next(request) }), ) diff --git a/apps/web/src/lib/services/common/api-client.ts b/apps/web/src/lib/services/common/api-client.ts index ee9502535..b8a769bc3 100644 --- a/apps/web/src/lib/services/common/api-client.ts +++ b/apps/web/src/lib/services/common/api-client.ts @@ -4,7 +4,7 @@ * @description HTTP client that uses Bearer tokens for desktop and cookies for web */ -import { FetchHttpClient, HttpClient } from "effect/unstable/http" +import { FetchHttpClient, HttpClient, HttpClientError } from "effect/unstable/http" import { HttpApiClient } from "effect/unstable/httpapi" import { HazelApi } from "@hazel/domain/http" import { ServiceMap, Layer } from "effect" @@ -25,14 +25,11 @@ export class ApiClient extends ServiceMap.Service()("ApiClient", { times: 3, // Only retry server errors (5xx), not client errors (4xx) like 401/403 while: (error) => { - if (error._tag === "HttpClientError") { - const status = error.response.status - // Only retry server errors (500-599) and network errors - // Don't retry client errors (400-499) including auth errors - return status >= 500 && status < 600 + if (HttpClientError.isHttpClientError(error)) { + const status = error.response?.status + return status === undefined || (status >= 500 && status < 600) } - // Retry other transient errors (network issues, etc.) - return error._tag === "HttpClientError" + return false }, }), ), diff --git a/apps/web/src/lib/services/common/network-mode.ts b/apps/web/src/lib/services/common/network-mode.ts index f68e3eb88..d816a19fc 100644 --- a/apps/web/src/lib/services/common/network-mode.ts +++ b/apps/web/src/lib/services/common/network-mode.ts @@ -1,24 +1,40 @@ -import * as Chunk from "effect/Chunk" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" +import * as Latch from "effect/Latch" +import * as Queue from "effect/Queue" import * as ServiceMap from "effect/ServiceMap" import * as Stream from "effect/Stream" import * as SubscriptionRef from "effect/SubscriptionRef" export class NetworkMonitor extends ServiceMap.Service()("NetworkMonitor", { make: Effect.gen(function* () { - const latch = yield* Effect.makeLatch(true) + const latch = yield* Latch.make(true) const ref = yield* SubscriptionRef.make(window.navigator.onLine) - yield* Stream.async((emit) => { - const onlineHandler = () => emit(Effect.succeed(Chunk.of(true))) - const offlineHandler = () => emit(Effect.succeed(Chunk.of(false))) - window.addEventListener("online", onlineHandler) - window.addEventListener("offline", offlineHandler) - }).pipe( + yield* Stream.callback((queue) => + Effect.gen(function* () { + const onlineHandler = () => { + Effect.runFork(Queue.offer(queue, true)) + } + const offlineHandler = () => { + Effect.runFork(Queue.offer(queue, false)) + } + window.addEventListener("online", onlineHandler) + window.addEventListener("offline", offlineHandler) + yield* Effect.addFinalizer(() => + Effect.sync(() => { + window.removeEventListener("online", onlineHandler) + window.removeEventListener("offline", offlineHandler) + }), + ) + // Keep scope alive + yield* Effect.never + }), + ).pipe( Stream.tap((isOnline) => - (isOnline ? latch.open : latch.close).pipe( - Effect.zipRight(SubscriptionRef.update(ref, () => isOnline)), + Effect.andThen( + isOnline ? latch.open : latch.close, + SubscriptionRef.update(ref, () => isOnline), ), ), Stream.runDrain, diff --git a/apps/web/src/lib/services/common/rpc-atom-client.ts b/apps/web/src/lib/services/common/rpc-atom-client.ts index 03de43d77..c3d3f712e 100644 --- a/apps/web/src/lib/services/common/rpc-atom-client.ts +++ b/apps/web/src/lib/services/common/rpc-atom-client.ts @@ -28,11 +28,6 @@ import { UserRpcs, } from "@hazel/domain/rpc" import { Layer } from "effect" -import { - createRpcTypeResolver, - DevtoolsProtocolLayer, - setRpcTypeResolver, -} from "effect-rpc-tanstack-devtools" const backendUrl = import.meta.env.VITE_BACKEND_URL const httpUrl = `${backendUrl}/rpc` @@ -47,7 +42,7 @@ export const RpcProtocolLive = BaseProtocolLive // Use Layer.mergeAll to make AuthMiddlewareClientLive available alongside the protocol const AtomRpcProtocolLive = Layer.mergeAll(RpcProtocolLive, AuthMiddlewareClientLive, Reactivity.layer) -const AllRpcs = MessageRpcs.merge( +const BaseRpcs = MessageRpcs.merge( NotificationRpcs, InvitationRpcs, IntegrationRequestRpcs, @@ -67,18 +62,12 @@ const AllRpcs = MessageRpcs.merge( AttachmentRpcs, UserPresenceStatusRpcs, BotRpcs, - ChatSyncRpcs, ConnectShareRpcs, ) -// Configure RPC type resolver for devtools (only in dev mode) -if (import.meta.env.DEV) { - setRpcTypeResolver(createRpcTypeResolver([AllRpcs])) -} - +const AllRpcs = BaseRpcs.merge(ChatSyncRpcs) export class HazelRpcClient extends AtomRpc.Service()("HazelRpcClient", { group: AllRpcs, - // @ts-expect-error protocol: AtomRpcProtocolLive, }) {} diff --git a/apps/web/src/lib/services/common/runtime.ts b/apps/web/src/lib/services/common/runtime.ts index 4fe11ccfb..56c0cc107 100644 --- a/apps/web/src/lib/services/common/runtime.ts +++ b/apps/web/src/lib/services/common/runtime.ts @@ -28,4 +28,4 @@ export const runtimeLayer = Layer.mergeAll(ApiClient.layer, HazelRpcClient.layer * * Used by collections.ts and other imperative code that calls runtime.runPromise(). */ -export const runtime = ManagedRuntime.make(runtimeLayer, Atom.defaultMemoMap) +export const runtime = ManagedRuntime.make(runtimeLayer, { memoMap: Atom.defaultMemoMap }) diff --git a/apps/web/src/lib/services/desktop/tauri-auth.ts b/apps/web/src/lib/services/desktop/tauri-auth.ts index 0ff3fb278..91cffd5d0 100644 --- a/apps/web/src/lib/services/desktop/tauri-auth.ts +++ b/apps/web/src/lib/services/desktop/tauri-auth.ts @@ -22,7 +22,7 @@ import { TauriCommandError, TauriNotAvailableError, } from "@hazel/domain/errors" -import { ServiceMap, Deferred, Duration, Effect, FiberId, Layer } from "effect" +import { ServiceMap, Deferred, Duration, Effect, Fiber, Layer } from "effect" import { TokenExchange } from "./token-exchange" import { TokenStorage } from "./token-storage" @@ -148,25 +148,21 @@ export class TauriAuth extends ServiceMap.Service()("TauriAuth", { yield* Effect.log("[tauri-auth] Browser opened, waiting for web callback...") // Wait for OAuth callback with timeout and cleanup - // Uses Deferred to ensure cleanup works even if listener setup is async - const callbackUrl = yield* Effect.async((resume) => { - const unlistenDeferred = Deferred.unsafeMake<() => void, never>(FiberId.none) + const callbackUrl = yield* Effect.callback((resume) => { + let unlistenFn: (() => void) | null = null event - .listen("oauth-callback", (evt) => { + .listen("oauth-callback", (evt: { payload: string }) => { resume(Effect.succeed(evt.payload)) }) - .then((unlistenFn) => { - Deferred.unsafeDone(unlistenDeferred, Effect.succeed(unlistenFn)) + .then((fn: () => void) => { + unlistenFn = fn }) - // Return cleanup function that waits for unlisten to be available - return Deferred.await(unlistenDeferred).pipe( - Effect.flatMap((fn) => Effect.sync(() => fn())), - // If deferred isn't done yet, just skip cleanup - Effect.timeout(Duration.millis(100)), - Effect.ignore, - ) + // Return cleanup effect + return Effect.sync(() => { + if (unlistenFn) unlistenFn() + }) }).pipe( Effect.timeout(Duration.minutes(2)), Effect.catchTag("TimeoutError", () => diff --git a/apps/web/src/lib/services/desktop/token-exchange.ts b/apps/web/src/lib/services/desktop/token-exchange.ts index 394a23cec..4782a674a 100644 --- a/apps/web/src/lib/services/desktop/token-exchange.ts +++ b/apps/web/src/lib/services/desktop/token-exchange.ts @@ -4,13 +4,27 @@ * @description HTTP client for token exchange using Effect HttpClient with Schema validation */ -import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { FetchHttpClient, HttpBody, HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http" import { OAuthCodeExpiredError, TokenDecodeError, TokenExchangeError } from "@hazel/domain/errors" import { RefreshTokenResponse, TokenResponse } from "@hazel/domain/http" import { ServiceMap, Duration, Effect, Layer, Schema } from "effect" const DEFAULT_TIMEOUT = Duration.seconds(60) +const mapHttpClientError = (context: string) => (error: HttpClientError.HttpClientError) => + Effect.fail( + new TokenExchangeError({ + message: + error.response?.status === undefined + ? `Network error during ${context}` + : `Server error during ${context}`, + detail: + error.response?.status === undefined + ? String(error) + : `HTTP ${error.response.status}`, + }), + ) + export class TokenExchange extends ServiceMap.Service()("TokenExchange", { make: Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient @@ -80,7 +94,6 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc ), ) }).pipe( - // Map HTTP client errors to TokenExchangeError Effect.catchTag("TimeoutError", () => Effect.fail( new TokenExchangeError({ @@ -88,22 +101,7 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc }), ), ), - Effect.catchTag("HttpClientError", (error) => - Effect.fail( - new TokenExchangeError({ - message: "Network error during token exchange", - detail: String(error), - }), - ), - ), - Effect.catchTag("HttpClientError", (error) => - Effect.fail( - new TokenExchangeError({ - message: "Server error during token exchange", - detail: `HTTP ${error.response.status}`, - }), - ), - ), + Effect.catchIf(HttpClientError.isHttpClientError, mapHttpClientError("token exchange")), ), /** @@ -143,7 +141,6 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc ), ) }).pipe( - // Map HTTP client errors to TokenExchangeError Effect.catchTag("TimeoutError", () => Effect.fail( new TokenExchangeError({ @@ -151,22 +148,7 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc }), ), ), - Effect.catchTag("HttpClientError", (error) => - Effect.fail( - new TokenExchangeError({ - message: "Network error during token refresh", - detail: String(error), - }), - ), - ), - Effect.catchTag("HttpClientError", (error) => - Effect.fail( - new TokenExchangeError({ - message: "Server error during token refresh", - detail: `HTTP ${error.response.status}`, - }), - ), - ), + Effect.catchIf(HttpClientError.isHttpClientError, mapHttpClientError("token refresh")), ), } }), diff --git a/apps/web/src/lib/toast-exit.tsx b/apps/web/src/lib/toast-exit.tsx index d63640404..ab0a25966 100644 --- a/apps/web/src/lib/toast-exit.tsx +++ b/apps/web/src/lib/toast-exit.tsx @@ -1,4 +1,4 @@ -import { Cause, Chunk, Exit, Option } from "effect" +import { Cause, Exit, Option } from "effect" import { type ExternalToast, toast } from "sonner" import { @@ -118,9 +118,8 @@ export type ExitToastBuilder = { /** * Execute the builder and show appropriate toast. - * Only available when all non-common errors are handled. */ - run: NonCommonErrorTags extends HandledErrors ? () => void : BuilderNotReady + run: () => void } /** @@ -187,9 +186,7 @@ export type ExitToastAsyncBuilder = { * Execute the builder, await the promise, and show appropriate toast. * Returns the Exit for further handling. */ - run: NonCommonErrorTags extends HandledErrors - ? () => Promise> - : BuilderNotReady + run: () => Promise> } /** @@ -233,13 +230,12 @@ function executeToast( toastOptions.id = loadingToastId } - const failures = Cause.failures(cause) - const firstFailure = Chunk.head(failures) + const firstFailure = Cause.findErrorOption(cause) let userError: UserErrorMessage if (Option.isSome(firstFailure)) { - userError = getErrorMessage(firstFailure.value, state.errorHandlers) + userError = getErrorMessage(firstFailure.value as { _tag: string }, state.errorHandlers) } else { userError = getUserFriendlyError(cause) } diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index d4eb0d2d4..97e3cf499 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -8,1362 +8,1420 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { Route as rootRouteImport } from "./routes/__root" -import { Route as DevLayoutRouteImport } from "./routes/_dev/layout" -import { Route as AppLayoutRouteImport } from "./routes/_app/layout" -import { Route as AppIndexRouteImport } from "./routes/_app/index" -import { Route as JoinSlugRouteImport } from "./routes/join/$slug" -import { Route as AuthLoginRouteImport } from "./routes/auth/login" -import { Route as AuthDesktopLoginRouteImport } from "./routes/auth/desktop-login" -import { Route as AuthDesktopCallbackRouteImport } from "./routes/auth/desktop-callback" -import { Route as AuthCallbackRouteImport } from "./routes/auth/callback" -import { Route as DevUiLayoutRouteImport } from "./routes/_dev/ui/layout" -import { Route as AppOrgSlugLayoutRouteImport } from "./routes/_app/$orgSlug/layout" -import { Route as DevEmbedsIndexRouteImport } from "./routes/dev/embeds/index" -import { Route as AppSelectOrganizationIndexRouteImport } from "./routes/_app/select-organization/index" -import { Route as AppOnboardingIndexRouteImport } from "./routes/_app/onboarding/index" -import { Route as AppOrgSlugIndexRouteImport } from "./routes/_app/$orgSlug/index" -import { Route as DevEmbedsRailwayRouteImport } from "./routes/dev/embeds/railway" -import { Route as DevEmbedsOpenstatusRouteImport } from "./routes/dev/embeds/openstatus" -import { Route as DevEmbedsGithubRouteImport } from "./routes/dev/embeds/github" -import { Route as DevEmbedsDemoRouteImport } from "./routes/dev/embeds/demo" -import { Route as DevUiAgentStepsRouteImport } from "./routes/_dev/ui/agent-steps" -import { Route as AppOnboardingSetupOrganizationRouteImport } from "./routes/_app/onboarding/setup-organization" -import { Route as AppOrgSlugSettingsLayoutRouteImport } from "./routes/_app/$orgSlug/settings/layout" -import { Route as AppOrgSlugNotificationsLayoutRouteImport } from "./routes/_app/$orgSlug/notifications/layout" -import { Route as AppOrgSlugMySettingsLayoutRouteImport } from "./routes/_app/$orgSlug/my-settings/layout" -import { Route as AppOrgSlugSettingsIndexRouteImport } from "./routes/_app/$orgSlug/settings/index" -import { Route as AppOrgSlugNotificationsIndexRouteImport } from "./routes/_app/$orgSlug/notifications/index" -import { Route as AppOrgSlugMySettingsIndexRouteImport } from "./routes/_app/$orgSlug/my-settings/index" -import { Route as AppOrgSlugChatIndexRouteImport } from "./routes/_app/$orgSlug/chat/index" -import { Route as AppOrgSlugSettingsTeamRouteImport } from "./routes/_app/$orgSlug/settings/team" -import { Route as AppOrgSlugSettingsInvitationsRouteImport } from "./routes/_app/$orgSlug/settings/invitations" -import { Route as AppOrgSlugSettingsDebugRouteImport } from "./routes/_app/$orgSlug/settings/debug" -import { Route as AppOrgSlugSettingsCustomEmojisRouteImport } from "./routes/_app/$orgSlug/settings/custom-emojis" -import { Route as AppOrgSlugSettingsConnectInvitesRouteImport } from "./routes/_app/$orgSlug/settings/connect-invites" -import { Route as AppOrgSlugSettingsAuthenticationRouteImport } from "./routes/_app/$orgSlug/settings/authentication" -import { Route as AppOrgSlugProfileUserIdRouteImport } from "./routes/_app/$orgSlug/profile/$userId" -import { Route as AppOrgSlugNotificationsThreadsRouteImport } from "./routes/_app/$orgSlug/notifications/threads" -import { Route as AppOrgSlugNotificationsGeneralRouteImport } from "./routes/_app/$orgSlug/notifications/general" -import { Route as AppOrgSlugNotificationsDmsRouteImport } from "./routes/_app/$orgSlug/notifications/dms" -import { Route as AppOrgSlugMySettingsProfileRouteImport } from "./routes/_app/$orgSlug/my-settings/profile" -import { Route as AppOrgSlugMySettingsNotificationsRouteImport } from "./routes/_app/$orgSlug/my-settings/notifications" -import { Route as AppOrgSlugMySettingsLinkedAccountsRouteImport } from "./routes/_app/$orgSlug/my-settings/linked-accounts" -import { Route as AppOrgSlugMySettingsDesktopRouteImport } from "./routes/_app/$orgSlug/my-settings/desktop" -import { Route as AppOrgSlugChatIdRouteImport } from "./routes/_app/$orgSlug/chat/$id" -import { Route as AppOrgSlugSettingsIntegrationsLayoutRouteImport } from "./routes/_app/$orgSlug/settings/integrations/layout" -import { Route as AppOrgSlugSettingsChatSyncLayoutRouteImport } from "./routes/_app/$orgSlug/settings/chat-sync/layout" -import { Route as AppOrgSlugSettingsIntegrationsIndexRouteImport } from "./routes/_app/$orgSlug/settings/integrations/index" -import { Route as AppOrgSlugSettingsChatSyncIndexRouteImport } from "./routes/_app/$orgSlug/settings/chat-sync/index" -import { Route as AppOrgSlugChatIdIndexRouteImport } from "./routes/_app/$orgSlug/chat/$id/index" -import { Route as AppOrgSlugSettingsIntegrationsYourAppsRouteImport } from "./routes/_app/$orgSlug/settings/integrations/your-apps" -import { Route as AppOrgSlugSettingsIntegrationsMarketplaceRouteImport } from "./routes/_app/$orgSlug/settings/integrations/marketplace" -import { Route as AppOrgSlugSettingsIntegrationsInstalledRouteImport } from "./routes/_app/$orgSlug/settings/integrations/installed" -import { Route as AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport } from "./routes/_app/$orgSlug/settings/integrations/$integrationId" -import { Route as AppOrgSlugSettingsChatSyncConnectionIdRouteImport } from "./routes/_app/$orgSlug/settings/chat-sync/$connectionId" -import { Route as AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/layout" -import { Route as AppOrgSlugChatIdFilesIndexRouteImport } from "./routes/_app/$orgSlug/chat/$id/files/index" -import { Route as AppOrgSlugChannelsChannelIdSettingsIndexRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/index" -import { Route as AppOrgSlugChatIdFilesMediaRouteImport } from "./routes/_app/$orgSlug/chat/$id/files/media" -import { Route as AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/overview" -import { Route as AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/integrations" -import { Route as AppOrgSlugChannelsChannelIdSettingsConnectRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/connect" +import { Route as rootRouteImport } from './routes/__root' +import { Route as DevLayoutRouteImport } from './routes/_dev/layout' +import { Route as AppLayoutRouteImport } from './routes/_app/layout' +import { Route as AppIndexRouteImport } from './routes/_app/index' +import { Route as JoinSlugRouteImport } from './routes/join/$slug' +import { Route as AuthLoginRouteImport } from './routes/auth/login' +import { Route as AuthDesktopLoginRouteImport } from './routes/auth/desktop-login' +import { Route as AuthDesktopCallbackRouteImport } from './routes/auth/desktop-callback' +import { Route as AuthCallbackRouteImport } from './routes/auth/callback' +import { Route as DevUiLayoutRouteImport } from './routes/_dev/ui/layout' +import { Route as AppOrgSlugLayoutRouteImport } from './routes/_app/$orgSlug/layout' +import { Route as DevEmbedsIndexRouteImport } from './routes/dev/embeds/index' +import { Route as AppSelectOrganizationIndexRouteImport } from './routes/_app/select-organization/index' +import { Route as AppOnboardingIndexRouteImport } from './routes/_app/onboarding/index' +import { Route as AppOrgSlugIndexRouteImport } from './routes/_app/$orgSlug/index' +import { Route as DevEmbedsRailwayRouteImport } from './routes/dev/embeds/railway' +import { Route as DevEmbedsOpenstatusRouteImport } from './routes/dev/embeds/openstatus' +import { Route as DevEmbedsGithubRouteImport } from './routes/dev/embeds/github' +import { Route as DevEmbedsDemoRouteImport } from './routes/dev/embeds/demo' +import { Route as DevUiAgentStepsRouteImport } from './routes/_dev/ui/agent-steps' +import { Route as AppOnboardingSetupOrganizationRouteImport } from './routes/_app/onboarding/setup-organization' +import { Route as AppOrgSlugSettingsLayoutRouteImport } from './routes/_app/$orgSlug/settings/layout' +import { Route as AppOrgSlugNotificationsLayoutRouteImport } from './routes/_app/$orgSlug/notifications/layout' +import { Route as AppOrgSlugMySettingsLayoutRouteImport } from './routes/_app/$orgSlug/my-settings/layout' +import { Route as AppOrgSlugSettingsIndexRouteImport } from './routes/_app/$orgSlug/settings/index' +import { Route as AppOrgSlugNotificationsIndexRouteImport } from './routes/_app/$orgSlug/notifications/index' +import { Route as AppOrgSlugMySettingsIndexRouteImport } from './routes/_app/$orgSlug/my-settings/index' +import { Route as AppOrgSlugChatIndexRouteImport } from './routes/_app/$orgSlug/chat/index' +import { Route as AppOrgSlugSettingsTeamRouteImport } from './routes/_app/$orgSlug/settings/team' +import { Route as AppOrgSlugSettingsInvitationsRouteImport } from './routes/_app/$orgSlug/settings/invitations' +import { Route as AppOrgSlugSettingsDebugRouteImport } from './routes/_app/$orgSlug/settings/debug' +import { Route as AppOrgSlugSettingsCustomEmojisRouteImport } from './routes/_app/$orgSlug/settings/custom-emojis' +import { Route as AppOrgSlugSettingsConnectInvitesRouteImport } from './routes/_app/$orgSlug/settings/connect-invites' +import { Route as AppOrgSlugSettingsAuthenticationRouteImport } from './routes/_app/$orgSlug/settings/authentication' +import { Route as AppOrgSlugProfileUserIdRouteImport } from './routes/_app/$orgSlug/profile/$userId' +import { Route as AppOrgSlugNotificationsThreadsRouteImport } from './routes/_app/$orgSlug/notifications/threads' +import { Route as AppOrgSlugNotificationsGeneralRouteImport } from './routes/_app/$orgSlug/notifications/general' +import { Route as AppOrgSlugNotificationsDmsRouteImport } from './routes/_app/$orgSlug/notifications/dms' +import { Route as AppOrgSlugMySettingsProfileRouteImport } from './routes/_app/$orgSlug/my-settings/profile' +import { Route as AppOrgSlugMySettingsNotificationsRouteImport } from './routes/_app/$orgSlug/my-settings/notifications' +import { Route as AppOrgSlugMySettingsLinkedAccountsRouteImport } from './routes/_app/$orgSlug/my-settings/linked-accounts' +import { Route as AppOrgSlugMySettingsDesktopRouteImport } from './routes/_app/$orgSlug/my-settings/desktop' +import { Route as AppOrgSlugChatIdRouteImport } from './routes/_app/$orgSlug/chat/$id' +import { Route as AppOrgSlugSettingsIntegrationsLayoutRouteImport } from './routes/_app/$orgSlug/settings/integrations/layout' +import { Route as AppOrgSlugSettingsChatSyncLayoutRouteImport } from './routes/_app/$orgSlug/settings/chat-sync/layout' +import { Route as AppOrgSlugSettingsIntegrationsIndexRouteImport } from './routes/_app/$orgSlug/settings/integrations/index' +import { Route as AppOrgSlugSettingsChatSyncIndexRouteImport } from './routes/_app/$orgSlug/settings/chat-sync/index' +import { Route as AppOrgSlugChatIdIndexRouteImport } from './routes/_app/$orgSlug/chat/$id/index' +import { Route as AppOrgSlugSettingsIntegrationsYourAppsRouteImport } from './routes/_app/$orgSlug/settings/integrations/your-apps' +import { Route as AppOrgSlugSettingsIntegrationsMarketplaceRouteImport } from './routes/_app/$orgSlug/settings/integrations/marketplace' +import { Route as AppOrgSlugSettingsIntegrationsInstalledRouteImport } from './routes/_app/$orgSlug/settings/integrations/installed' +import { Route as AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport } from './routes/_app/$orgSlug/settings/integrations/$integrationId' +import { Route as AppOrgSlugSettingsChatSyncConnectionIdRouteImport } from './routes/_app/$orgSlug/settings/chat-sync/$connectionId' +import { Route as AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/layout' +import { Route as AppOrgSlugChatIdFilesIndexRouteImport } from './routes/_app/$orgSlug/chat/$id/files/index' +import { Route as AppOrgSlugChannelsChannelIdSettingsIndexRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/index' +import { Route as AppOrgSlugChatIdFilesMediaRouteImport } from './routes/_app/$orgSlug/chat/$id/files/media' +import { Route as AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/overview' +import { Route as AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/integrations' +import { Route as AppOrgSlugChannelsChannelIdSettingsConnectRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/connect' const DevLayoutRoute = DevLayoutRouteImport.update({ - id: "/_dev", - getParentRoute: () => rootRouteImport, + id: '/_dev', + getParentRoute: () => rootRouteImport, } as any) const AppLayoutRoute = AppLayoutRouteImport.update({ - id: "/_app", - getParentRoute: () => rootRouteImport, + id: '/_app', + getParentRoute: () => rootRouteImport, } as any) const AppIndexRoute = AppIndexRouteImport.update({ - id: "/", - path: "/", - getParentRoute: () => AppLayoutRoute, + id: '/', + path: '/', + getParentRoute: () => AppLayoutRoute, } as any) const JoinSlugRoute = JoinSlugRouteImport.update({ - id: "/join/$slug", - path: "/join/$slug", - getParentRoute: () => rootRouteImport, + id: '/join/$slug', + path: '/join/$slug', + getParentRoute: () => rootRouteImport, } as any) const AuthLoginRoute = AuthLoginRouteImport.update({ - id: "/auth/login", - path: "/auth/login", - getParentRoute: () => rootRouteImport, + id: '/auth/login', + path: '/auth/login', + getParentRoute: () => rootRouteImport, } as any) const AuthDesktopLoginRoute = AuthDesktopLoginRouteImport.update({ - id: "/auth/desktop-login", - path: "/auth/desktop-login", - getParentRoute: () => rootRouteImport, + id: '/auth/desktop-login', + path: '/auth/desktop-login', + getParentRoute: () => rootRouteImport, } as any) const AuthDesktopCallbackRoute = AuthDesktopCallbackRouteImport.update({ - id: "/auth/desktop-callback", - path: "/auth/desktop-callback", - getParentRoute: () => rootRouteImport, + id: '/auth/desktop-callback', + path: '/auth/desktop-callback', + getParentRoute: () => rootRouteImport, } as any) const AuthCallbackRoute = AuthCallbackRouteImport.update({ - id: "/auth/callback", - path: "/auth/callback", - getParentRoute: () => rootRouteImport, + id: '/auth/callback', + path: '/auth/callback', + getParentRoute: () => rootRouteImport, } as any) const DevUiLayoutRoute = DevUiLayoutRouteImport.update({ - id: "/ui", - path: "/ui", - getParentRoute: () => DevLayoutRoute, + id: '/ui', + path: '/ui', + getParentRoute: () => DevLayoutRoute, } as any) const AppOrgSlugLayoutRoute = AppOrgSlugLayoutRouteImport.update({ - id: "/$orgSlug", - path: "/$orgSlug", - getParentRoute: () => AppLayoutRoute, + id: '/$orgSlug', + path: '/$orgSlug', + getParentRoute: () => AppLayoutRoute, } as any) const DevEmbedsIndexRoute = DevEmbedsIndexRouteImport.update({ - id: "/dev/embeds/", - path: "/dev/embeds/", - getParentRoute: () => rootRouteImport, -} as any) -const AppSelectOrganizationIndexRoute = AppSelectOrganizationIndexRouteImport.update({ - id: "/select-organization/", - path: "/select-organization/", - getParentRoute: () => AppLayoutRoute, + id: '/dev/embeds/', + path: '/dev/embeds/', + getParentRoute: () => rootRouteImport, } as any) +const AppSelectOrganizationIndexRoute = + AppSelectOrganizationIndexRouteImport.update({ + id: '/select-organization/', + path: '/select-organization/', + getParentRoute: () => AppLayoutRoute, + } as any) const AppOnboardingIndexRoute = AppOnboardingIndexRouteImport.update({ - id: "/onboarding/", - path: "/onboarding/", - getParentRoute: () => AppLayoutRoute, + id: '/onboarding/', + path: '/onboarding/', + getParentRoute: () => AppLayoutRoute, } as any) const AppOrgSlugIndexRoute = AppOrgSlugIndexRouteImport.update({ - id: "/", - path: "/", - getParentRoute: () => AppOrgSlugLayoutRoute, + id: '/', + path: '/', + getParentRoute: () => AppOrgSlugLayoutRoute, } as any) const DevEmbedsRailwayRoute = DevEmbedsRailwayRouteImport.update({ - id: "/dev/embeds/railway", - path: "/dev/embeds/railway", - getParentRoute: () => rootRouteImport, + id: '/dev/embeds/railway', + path: '/dev/embeds/railway', + getParentRoute: () => rootRouteImport, } as any) const DevEmbedsOpenstatusRoute = DevEmbedsOpenstatusRouteImport.update({ - id: "/dev/embeds/openstatus", - path: "/dev/embeds/openstatus", - getParentRoute: () => rootRouteImport, + id: '/dev/embeds/openstatus', + path: '/dev/embeds/openstatus', + getParentRoute: () => rootRouteImport, } as any) const DevEmbedsGithubRoute = DevEmbedsGithubRouteImport.update({ - id: "/dev/embeds/github", - path: "/dev/embeds/github", - getParentRoute: () => rootRouteImport, + id: '/dev/embeds/github', + path: '/dev/embeds/github', + getParentRoute: () => rootRouteImport, } as any) const DevEmbedsDemoRoute = DevEmbedsDemoRouteImport.update({ - id: "/dev/embeds/demo", - path: "/dev/embeds/demo", - getParentRoute: () => rootRouteImport, + id: '/dev/embeds/demo', + path: '/dev/embeds/demo', + getParentRoute: () => rootRouteImport, } as any) const DevUiAgentStepsRoute = DevUiAgentStepsRouteImport.update({ - id: "/agent-steps", - path: "/agent-steps", - getParentRoute: () => DevUiLayoutRoute, -} as any) -const AppOnboardingSetupOrganizationRoute = AppOnboardingSetupOrganizationRouteImport.update({ - id: "/onboarding/setup-organization", - path: "/onboarding/setup-organization", - getParentRoute: () => AppLayoutRoute, -} as any) -const AppOrgSlugSettingsLayoutRoute = AppOrgSlugSettingsLayoutRouteImport.update({ - id: "/settings", - path: "/settings", - getParentRoute: () => AppOrgSlugLayoutRoute, -} as any) -const AppOrgSlugNotificationsLayoutRoute = AppOrgSlugNotificationsLayoutRouteImport.update({ - id: "/notifications", - path: "/notifications", - getParentRoute: () => AppOrgSlugLayoutRoute, -} as any) -const AppOrgSlugMySettingsLayoutRoute = AppOrgSlugMySettingsLayoutRouteImport.update({ - id: "/my-settings", - path: "/my-settings", - getParentRoute: () => AppOrgSlugLayoutRoute, + id: '/agent-steps', + path: '/agent-steps', + getParentRoute: () => DevUiLayoutRoute, } as any) +const AppOnboardingSetupOrganizationRoute = + AppOnboardingSetupOrganizationRouteImport.update({ + id: '/onboarding/setup-organization', + path: '/onboarding/setup-organization', + getParentRoute: () => AppLayoutRoute, + } as any) +const AppOrgSlugSettingsLayoutRoute = + AppOrgSlugSettingsLayoutRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => AppOrgSlugLayoutRoute, + } as any) +const AppOrgSlugNotificationsLayoutRoute = + AppOrgSlugNotificationsLayoutRouteImport.update({ + id: '/notifications', + path: '/notifications', + getParentRoute: () => AppOrgSlugLayoutRoute, + } as any) +const AppOrgSlugMySettingsLayoutRoute = + AppOrgSlugMySettingsLayoutRouteImport.update({ + id: '/my-settings', + path: '/my-settings', + getParentRoute: () => AppOrgSlugLayoutRoute, + } as any) const AppOrgSlugSettingsIndexRoute = AppOrgSlugSettingsIndexRouteImport.update({ - id: "/", - path: "/", - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, -} as any) -const AppOrgSlugNotificationsIndexRoute = AppOrgSlugNotificationsIndexRouteImport.update({ - id: "/", - path: "/", - getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, -} as any) -const AppOrgSlugMySettingsIndexRoute = AppOrgSlugMySettingsIndexRouteImport.update({ - id: "/", - path: "/", - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, + id: '/', + path: '/', + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, } as any) +const AppOrgSlugNotificationsIndexRoute = + AppOrgSlugNotificationsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, + } as any) +const AppOrgSlugMySettingsIndexRoute = + AppOrgSlugMySettingsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, + } as any) const AppOrgSlugChatIndexRoute = AppOrgSlugChatIndexRouteImport.update({ - id: "/chat/", - path: "/chat/", - getParentRoute: () => AppOrgSlugLayoutRoute, + id: '/chat/', + path: '/chat/', + getParentRoute: () => AppOrgSlugLayoutRoute, } as any) const AppOrgSlugSettingsTeamRoute = AppOrgSlugSettingsTeamRouteImport.update({ - id: "/team", - path: "/team", - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, -} as any) -const AppOrgSlugSettingsInvitationsRoute = AppOrgSlugSettingsInvitationsRouteImport.update({ - id: "/invitations", - path: "/invitations", - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + id: '/team', + path: '/team', + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, } as any) +const AppOrgSlugSettingsInvitationsRoute = + AppOrgSlugSettingsInvitationsRouteImport.update({ + id: '/invitations', + path: '/invitations', + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + } as any) const AppOrgSlugSettingsDebugRoute = AppOrgSlugSettingsDebugRouteImport.update({ - id: "/debug", - path: "/debug", - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, -} as any) -const AppOrgSlugSettingsCustomEmojisRoute = AppOrgSlugSettingsCustomEmojisRouteImport.update({ - id: "/custom-emojis", - path: "/custom-emojis", - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, -} as any) -const AppOrgSlugSettingsConnectInvitesRoute = AppOrgSlugSettingsConnectInvitesRouteImport.update({ - id: "/connect-invites", - path: "/connect-invites", - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, -} as any) -const AppOrgSlugSettingsAuthenticationRoute = AppOrgSlugSettingsAuthenticationRouteImport.update({ - id: "/authentication", - path: "/authentication", - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + id: '/debug', + path: '/debug', + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, } as any) +const AppOrgSlugSettingsCustomEmojisRoute = + AppOrgSlugSettingsCustomEmojisRouteImport.update({ + id: '/custom-emojis', + path: '/custom-emojis', + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + } as any) +const AppOrgSlugSettingsConnectInvitesRoute = + AppOrgSlugSettingsConnectInvitesRouteImport.update({ + id: '/connect-invites', + path: '/connect-invites', + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + } as any) +const AppOrgSlugSettingsAuthenticationRoute = + AppOrgSlugSettingsAuthenticationRouteImport.update({ + id: '/authentication', + path: '/authentication', + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + } as any) const AppOrgSlugProfileUserIdRoute = AppOrgSlugProfileUserIdRouteImport.update({ - id: "/profile/$userId", - path: "/profile/$userId", - getParentRoute: () => AppOrgSlugLayoutRoute, -} as any) -const AppOrgSlugNotificationsThreadsRoute = AppOrgSlugNotificationsThreadsRouteImport.update({ - id: "/threads", - path: "/threads", - getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, -} as any) -const AppOrgSlugNotificationsGeneralRoute = AppOrgSlugNotificationsGeneralRouteImport.update({ - id: "/general", - path: "/general", - getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, -} as any) -const AppOrgSlugNotificationsDmsRoute = AppOrgSlugNotificationsDmsRouteImport.update({ - id: "/dms", - path: "/dms", - getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, -} as any) -const AppOrgSlugMySettingsProfileRoute = AppOrgSlugMySettingsProfileRouteImport.update({ - id: "/profile", - path: "/profile", - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, -} as any) -const AppOrgSlugMySettingsNotificationsRoute = AppOrgSlugMySettingsNotificationsRouteImport.update({ - id: "/notifications", - path: "/notifications", - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, -} as any) -const AppOrgSlugMySettingsLinkedAccountsRoute = AppOrgSlugMySettingsLinkedAccountsRouteImport.update({ - id: "/linked-accounts", - path: "/linked-accounts", - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, -} as any) -const AppOrgSlugMySettingsDesktopRoute = AppOrgSlugMySettingsDesktopRouteImport.update({ - id: "/desktop", - path: "/desktop", - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, + id: '/profile/$userId', + path: '/profile/$userId', + getParentRoute: () => AppOrgSlugLayoutRoute, } as any) +const AppOrgSlugNotificationsThreadsRoute = + AppOrgSlugNotificationsThreadsRouteImport.update({ + id: '/threads', + path: '/threads', + getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, + } as any) +const AppOrgSlugNotificationsGeneralRoute = + AppOrgSlugNotificationsGeneralRouteImport.update({ + id: '/general', + path: '/general', + getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, + } as any) +const AppOrgSlugNotificationsDmsRoute = + AppOrgSlugNotificationsDmsRouteImport.update({ + id: '/dms', + path: '/dms', + getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, + } as any) +const AppOrgSlugMySettingsProfileRoute = + AppOrgSlugMySettingsProfileRouteImport.update({ + id: '/profile', + path: '/profile', + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, + } as any) +const AppOrgSlugMySettingsNotificationsRoute = + AppOrgSlugMySettingsNotificationsRouteImport.update({ + id: '/notifications', + path: '/notifications', + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, + } as any) +const AppOrgSlugMySettingsLinkedAccountsRoute = + AppOrgSlugMySettingsLinkedAccountsRouteImport.update({ + id: '/linked-accounts', + path: '/linked-accounts', + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, + } as any) +const AppOrgSlugMySettingsDesktopRoute = + AppOrgSlugMySettingsDesktopRouteImport.update({ + id: '/desktop', + path: '/desktop', + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, + } as any) const AppOrgSlugChatIdRoute = AppOrgSlugChatIdRouteImport.update({ - id: "/chat/$id", - path: "/chat/$id", - getParentRoute: () => AppOrgSlugLayoutRoute, -} as any) -const AppOrgSlugSettingsIntegrationsLayoutRoute = AppOrgSlugSettingsIntegrationsLayoutRouteImport.update({ - id: "/integrations", - path: "/integrations", - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, -} as any) -const AppOrgSlugSettingsChatSyncLayoutRoute = AppOrgSlugSettingsChatSyncLayoutRouteImport.update({ - id: "/chat-sync", - path: "/chat-sync", - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, -} as any) -const AppOrgSlugSettingsIntegrationsIndexRoute = AppOrgSlugSettingsIntegrationsIndexRouteImport.update({ - id: "/", - path: "/", - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, -} as any) -const AppOrgSlugSettingsChatSyncIndexRoute = AppOrgSlugSettingsChatSyncIndexRouteImport.update({ - id: "/", - path: "/", - getParentRoute: () => AppOrgSlugSettingsChatSyncLayoutRoute, + id: '/chat/$id', + path: '/chat/$id', + getParentRoute: () => AppOrgSlugLayoutRoute, } as any) +const AppOrgSlugSettingsIntegrationsLayoutRoute = + AppOrgSlugSettingsIntegrationsLayoutRouteImport.update({ + id: '/integrations', + path: '/integrations', + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + } as any) +const AppOrgSlugSettingsChatSyncLayoutRoute = + AppOrgSlugSettingsChatSyncLayoutRouteImport.update({ + id: '/chat-sync', + path: '/chat-sync', + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + } as any) +const AppOrgSlugSettingsIntegrationsIndexRoute = + AppOrgSlugSettingsIntegrationsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, + } as any) +const AppOrgSlugSettingsChatSyncIndexRoute = + AppOrgSlugSettingsChatSyncIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => AppOrgSlugSettingsChatSyncLayoutRoute, + } as any) const AppOrgSlugChatIdIndexRoute = AppOrgSlugChatIdIndexRouteImport.update({ - id: "/", - path: "/", - getParentRoute: () => AppOrgSlugChatIdRoute, -} as any) -const AppOrgSlugSettingsIntegrationsYourAppsRoute = AppOrgSlugSettingsIntegrationsYourAppsRouteImport.update({ - id: "/your-apps", - path: "/your-apps", - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, + id: '/', + path: '/', + getParentRoute: () => AppOrgSlugChatIdRoute, } as any) +const AppOrgSlugSettingsIntegrationsYourAppsRoute = + AppOrgSlugSettingsIntegrationsYourAppsRouteImport.update({ + id: '/your-apps', + path: '/your-apps', + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, + } as any) const AppOrgSlugSettingsIntegrationsMarketplaceRoute = - AppOrgSlugSettingsIntegrationsMarketplaceRouteImport.update({ - id: "/marketplace", - path: "/marketplace", - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) + AppOrgSlugSettingsIntegrationsMarketplaceRouteImport.update({ + id: '/marketplace', + path: '/marketplace', + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, + } as any) const AppOrgSlugSettingsIntegrationsInstalledRoute = - AppOrgSlugSettingsIntegrationsInstalledRouteImport.update({ - id: "/installed", - path: "/installed", - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) + AppOrgSlugSettingsIntegrationsInstalledRouteImport.update({ + id: '/installed', + path: '/installed', + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, + } as any) const AppOrgSlugSettingsIntegrationsIntegrationIdRoute = - AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport.update({ - id: "/$integrationId", - path: "/$integrationId", - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) -const AppOrgSlugSettingsChatSyncConnectionIdRoute = AppOrgSlugSettingsChatSyncConnectionIdRouteImport.update({ - id: "/$connectionId", - path: "/$connectionId", - getParentRoute: () => AppOrgSlugSettingsChatSyncLayoutRoute, -} as any) + AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport.update({ + id: '/$integrationId', + path: '/$integrationId', + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, + } as any) +const AppOrgSlugSettingsChatSyncConnectionIdRoute = + AppOrgSlugSettingsChatSyncConnectionIdRouteImport.update({ + id: '/$connectionId', + path: '/$connectionId', + getParentRoute: () => AppOrgSlugSettingsChatSyncLayoutRoute, + } as any) const AppOrgSlugChannelsChannelIdSettingsLayoutRoute = - AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport.update({ - id: "/channels/$channelId/settings", - path: "/channels/$channelId/settings", - getParentRoute: () => AppOrgSlugLayoutRoute, - } as any) -const AppOrgSlugChatIdFilesIndexRoute = AppOrgSlugChatIdFilesIndexRouteImport.update({ - id: "/files/", - path: "/files/", - getParentRoute: () => AppOrgSlugChatIdRoute, -} as any) + AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport.update({ + id: '/channels/$channelId/settings', + path: '/channels/$channelId/settings', + getParentRoute: () => AppOrgSlugLayoutRoute, + } as any) +const AppOrgSlugChatIdFilesIndexRoute = + AppOrgSlugChatIdFilesIndexRouteImport.update({ + id: '/files/', + path: '/files/', + getParentRoute: () => AppOrgSlugChatIdRoute, + } as any) const AppOrgSlugChannelsChannelIdSettingsIndexRoute = - AppOrgSlugChannelsChannelIdSettingsIndexRouteImport.update({ - id: "/", - path: "/", - getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, - } as any) -const AppOrgSlugChatIdFilesMediaRoute = AppOrgSlugChatIdFilesMediaRouteImport.update({ - id: "/files/media", - path: "/files/media", - getParentRoute: () => AppOrgSlugChatIdRoute, -} as any) + AppOrgSlugChannelsChannelIdSettingsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, + } as any) +const AppOrgSlugChatIdFilesMediaRoute = + AppOrgSlugChatIdFilesMediaRouteImport.update({ + id: '/files/media', + path: '/files/media', + getParentRoute: () => AppOrgSlugChatIdRoute, + } as any) const AppOrgSlugChannelsChannelIdSettingsOverviewRoute = - AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport.update({ - id: "/overview", - path: "/overview", - getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport.update({ + id: '/overview', + path: '/overview', + getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, + } as any) const AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute = - AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport.update({ - id: "/integrations", - path: "/integrations", - getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport.update({ + id: '/integrations', + path: '/integrations', + getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, + } as any) const AppOrgSlugChannelsChannelIdSettingsConnectRoute = - AppOrgSlugChannelsChannelIdSettingsConnectRouteImport.update({ - id: "/connect", - path: "/connect", - getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsConnectRouteImport.update({ + id: '/connect', + path: '/connect', + getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, + } as any) export interface FileRoutesByFullPath { - "/": typeof AppIndexRoute - "/$orgSlug": typeof AppOrgSlugLayoutRouteWithChildren - "/ui": typeof DevUiLayoutRouteWithChildren - "/auth/callback": typeof AuthCallbackRoute - "/auth/desktop-callback": typeof AuthDesktopCallbackRoute - "/auth/desktop-login": typeof AuthDesktopLoginRoute - "/auth/login": typeof AuthLoginRoute - "/join/$slug": typeof JoinSlugRoute - "/$orgSlug/my-settings": typeof AppOrgSlugMySettingsLayoutRouteWithChildren - "/$orgSlug/notifications": typeof AppOrgSlugNotificationsLayoutRouteWithChildren - "/$orgSlug/settings": typeof AppOrgSlugSettingsLayoutRouteWithChildren - "/onboarding/setup-organization": typeof AppOnboardingSetupOrganizationRoute - "/ui/agent-steps": typeof DevUiAgentStepsRoute - "/dev/embeds/demo": typeof DevEmbedsDemoRoute - "/dev/embeds/github": typeof DevEmbedsGithubRoute - "/dev/embeds/openstatus": typeof DevEmbedsOpenstatusRoute - "/dev/embeds/railway": typeof DevEmbedsRailwayRoute - "/$orgSlug/": typeof AppOrgSlugIndexRoute - "/onboarding/": typeof AppOnboardingIndexRoute - "/select-organization/": typeof AppSelectOrganizationIndexRoute - "/dev/embeds/": typeof DevEmbedsIndexRoute - "/$orgSlug/settings/chat-sync": typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren - "/$orgSlug/settings/integrations": typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren - "/$orgSlug/chat/$id": typeof AppOrgSlugChatIdRouteWithChildren - "/$orgSlug/my-settings/desktop": typeof AppOrgSlugMySettingsDesktopRoute - "/$orgSlug/my-settings/linked-accounts": typeof AppOrgSlugMySettingsLinkedAccountsRoute - "/$orgSlug/my-settings/notifications": typeof AppOrgSlugMySettingsNotificationsRoute - "/$orgSlug/my-settings/profile": typeof AppOrgSlugMySettingsProfileRoute - "/$orgSlug/notifications/dms": typeof AppOrgSlugNotificationsDmsRoute - "/$orgSlug/notifications/general": typeof AppOrgSlugNotificationsGeneralRoute - "/$orgSlug/notifications/threads": typeof AppOrgSlugNotificationsThreadsRoute - "/$orgSlug/profile/$userId": typeof AppOrgSlugProfileUserIdRoute - "/$orgSlug/settings/authentication": typeof AppOrgSlugSettingsAuthenticationRoute - "/$orgSlug/settings/connect-invites": typeof AppOrgSlugSettingsConnectInvitesRoute - "/$orgSlug/settings/custom-emojis": typeof AppOrgSlugSettingsCustomEmojisRoute - "/$orgSlug/settings/debug": typeof AppOrgSlugSettingsDebugRoute - "/$orgSlug/settings/invitations": typeof AppOrgSlugSettingsInvitationsRoute - "/$orgSlug/settings/team": typeof AppOrgSlugSettingsTeamRoute - "/$orgSlug/chat/": typeof AppOrgSlugChatIndexRoute - "/$orgSlug/my-settings/": typeof AppOrgSlugMySettingsIndexRoute - "/$orgSlug/notifications/": typeof AppOrgSlugNotificationsIndexRoute - "/$orgSlug/settings/": typeof AppOrgSlugSettingsIndexRoute - "/$orgSlug/channels/$channelId/settings": typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren - "/$orgSlug/settings/chat-sync/$connectionId": typeof AppOrgSlugSettingsChatSyncConnectionIdRoute - "/$orgSlug/settings/integrations/$integrationId": typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute - "/$orgSlug/settings/integrations/installed": typeof AppOrgSlugSettingsIntegrationsInstalledRoute - "/$orgSlug/settings/integrations/marketplace": typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute - "/$orgSlug/settings/integrations/your-apps": typeof AppOrgSlugSettingsIntegrationsYourAppsRoute - "/$orgSlug/chat/$id/": typeof AppOrgSlugChatIdIndexRoute - "/$orgSlug/settings/chat-sync/": typeof AppOrgSlugSettingsChatSyncIndexRoute - "/$orgSlug/settings/integrations/": typeof AppOrgSlugSettingsIntegrationsIndexRoute - "/$orgSlug/channels/$channelId/settings/connect": typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute - "/$orgSlug/channels/$channelId/settings/integrations": typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute - "/$orgSlug/channels/$channelId/settings/overview": typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute - "/$orgSlug/chat/$id/files/media": typeof AppOrgSlugChatIdFilesMediaRoute - "/$orgSlug/channels/$channelId/settings/": typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute - "/$orgSlug/chat/$id/files/": typeof AppOrgSlugChatIdFilesIndexRoute + '/': typeof AppIndexRoute + '/$orgSlug': typeof AppOrgSlugLayoutRouteWithChildren + '/ui': typeof DevUiLayoutRouteWithChildren + '/auth/callback': typeof AuthCallbackRoute + '/auth/desktop-callback': typeof AuthDesktopCallbackRoute + '/auth/desktop-login': typeof AuthDesktopLoginRoute + '/auth/login': typeof AuthLoginRoute + '/join/$slug': typeof JoinSlugRoute + '/$orgSlug/my-settings': typeof AppOrgSlugMySettingsLayoutRouteWithChildren + '/$orgSlug/notifications': typeof AppOrgSlugNotificationsLayoutRouteWithChildren + '/$orgSlug/settings': typeof AppOrgSlugSettingsLayoutRouteWithChildren + '/onboarding/setup-organization': typeof AppOnboardingSetupOrganizationRoute + '/ui/agent-steps': typeof DevUiAgentStepsRoute + '/dev/embeds/demo': typeof DevEmbedsDemoRoute + '/dev/embeds/github': typeof DevEmbedsGithubRoute + '/dev/embeds/openstatus': typeof DevEmbedsOpenstatusRoute + '/dev/embeds/railway': typeof DevEmbedsRailwayRoute + '/$orgSlug/': typeof AppOrgSlugIndexRoute + '/onboarding/': typeof AppOnboardingIndexRoute + '/select-organization/': typeof AppSelectOrganizationIndexRoute + '/dev/embeds/': typeof DevEmbedsIndexRoute + '/$orgSlug/settings/chat-sync': typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren + '/$orgSlug/settings/integrations': typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren + '/$orgSlug/chat/$id': typeof AppOrgSlugChatIdRouteWithChildren + '/$orgSlug/my-settings/desktop': typeof AppOrgSlugMySettingsDesktopRoute + '/$orgSlug/my-settings/linked-accounts': typeof AppOrgSlugMySettingsLinkedAccountsRoute + '/$orgSlug/my-settings/notifications': typeof AppOrgSlugMySettingsNotificationsRoute + '/$orgSlug/my-settings/profile': typeof AppOrgSlugMySettingsProfileRoute + '/$orgSlug/notifications/dms': typeof AppOrgSlugNotificationsDmsRoute + '/$orgSlug/notifications/general': typeof AppOrgSlugNotificationsGeneralRoute + '/$orgSlug/notifications/threads': typeof AppOrgSlugNotificationsThreadsRoute + '/$orgSlug/profile/$userId': typeof AppOrgSlugProfileUserIdRoute + '/$orgSlug/settings/authentication': typeof AppOrgSlugSettingsAuthenticationRoute + '/$orgSlug/settings/connect-invites': typeof AppOrgSlugSettingsConnectInvitesRoute + '/$orgSlug/settings/custom-emojis': typeof AppOrgSlugSettingsCustomEmojisRoute + '/$orgSlug/settings/debug': typeof AppOrgSlugSettingsDebugRoute + '/$orgSlug/settings/invitations': typeof AppOrgSlugSettingsInvitationsRoute + '/$orgSlug/settings/team': typeof AppOrgSlugSettingsTeamRoute + '/$orgSlug/chat/': typeof AppOrgSlugChatIndexRoute + '/$orgSlug/my-settings/': typeof AppOrgSlugMySettingsIndexRoute + '/$orgSlug/notifications/': typeof AppOrgSlugNotificationsIndexRoute + '/$orgSlug/settings/': typeof AppOrgSlugSettingsIndexRoute + '/$orgSlug/channels/$channelId/settings': typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren + '/$orgSlug/settings/chat-sync/$connectionId': typeof AppOrgSlugSettingsChatSyncConnectionIdRoute + '/$orgSlug/settings/integrations/$integrationId': typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute + '/$orgSlug/settings/integrations/installed': typeof AppOrgSlugSettingsIntegrationsInstalledRoute + '/$orgSlug/settings/integrations/marketplace': typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute + '/$orgSlug/settings/integrations/your-apps': typeof AppOrgSlugSettingsIntegrationsYourAppsRoute + '/$orgSlug/chat/$id/': typeof AppOrgSlugChatIdIndexRoute + '/$orgSlug/settings/chat-sync/': typeof AppOrgSlugSettingsChatSyncIndexRoute + '/$orgSlug/settings/integrations/': typeof AppOrgSlugSettingsIntegrationsIndexRoute + '/$orgSlug/channels/$channelId/settings/connect': typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute + '/$orgSlug/channels/$channelId/settings/integrations': typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute + '/$orgSlug/channels/$channelId/settings/overview': typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute + '/$orgSlug/chat/$id/files/media': typeof AppOrgSlugChatIdFilesMediaRoute + '/$orgSlug/channels/$channelId/settings/': typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute + '/$orgSlug/chat/$id/files/': typeof AppOrgSlugChatIdFilesIndexRoute } export interface FileRoutesByTo { - "/": typeof AppIndexRoute - "/ui": typeof DevUiLayoutRouteWithChildren - "/auth/callback": typeof AuthCallbackRoute - "/auth/desktop-callback": typeof AuthDesktopCallbackRoute - "/auth/desktop-login": typeof AuthDesktopLoginRoute - "/auth/login": typeof AuthLoginRoute - "/join/$slug": typeof JoinSlugRoute - "/onboarding/setup-organization": typeof AppOnboardingSetupOrganizationRoute - "/ui/agent-steps": typeof DevUiAgentStepsRoute - "/dev/embeds/demo": typeof DevEmbedsDemoRoute - "/dev/embeds/github": typeof DevEmbedsGithubRoute - "/dev/embeds/openstatus": typeof DevEmbedsOpenstatusRoute - "/dev/embeds/railway": typeof DevEmbedsRailwayRoute - "/$orgSlug": typeof AppOrgSlugIndexRoute - "/onboarding": typeof AppOnboardingIndexRoute - "/select-organization": typeof AppSelectOrganizationIndexRoute - "/dev/embeds": typeof DevEmbedsIndexRoute - "/$orgSlug/my-settings/desktop": typeof AppOrgSlugMySettingsDesktopRoute - "/$orgSlug/my-settings/linked-accounts": typeof AppOrgSlugMySettingsLinkedAccountsRoute - "/$orgSlug/my-settings/notifications": typeof AppOrgSlugMySettingsNotificationsRoute - "/$orgSlug/my-settings/profile": typeof AppOrgSlugMySettingsProfileRoute - "/$orgSlug/notifications/dms": typeof AppOrgSlugNotificationsDmsRoute - "/$orgSlug/notifications/general": typeof AppOrgSlugNotificationsGeneralRoute - "/$orgSlug/notifications/threads": typeof AppOrgSlugNotificationsThreadsRoute - "/$orgSlug/profile/$userId": typeof AppOrgSlugProfileUserIdRoute - "/$orgSlug/settings/authentication": typeof AppOrgSlugSettingsAuthenticationRoute - "/$orgSlug/settings/connect-invites": typeof AppOrgSlugSettingsConnectInvitesRoute - "/$orgSlug/settings/custom-emojis": typeof AppOrgSlugSettingsCustomEmojisRoute - "/$orgSlug/settings/debug": typeof AppOrgSlugSettingsDebugRoute - "/$orgSlug/settings/invitations": typeof AppOrgSlugSettingsInvitationsRoute - "/$orgSlug/settings/team": typeof AppOrgSlugSettingsTeamRoute - "/$orgSlug/chat": typeof AppOrgSlugChatIndexRoute - "/$orgSlug/my-settings": typeof AppOrgSlugMySettingsIndexRoute - "/$orgSlug/notifications": typeof AppOrgSlugNotificationsIndexRoute - "/$orgSlug/settings": typeof AppOrgSlugSettingsIndexRoute - "/$orgSlug/settings/chat-sync/$connectionId": typeof AppOrgSlugSettingsChatSyncConnectionIdRoute - "/$orgSlug/settings/integrations/$integrationId": typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute - "/$orgSlug/settings/integrations/installed": typeof AppOrgSlugSettingsIntegrationsInstalledRoute - "/$orgSlug/settings/integrations/marketplace": typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute - "/$orgSlug/settings/integrations/your-apps": typeof AppOrgSlugSettingsIntegrationsYourAppsRoute - "/$orgSlug/chat/$id": typeof AppOrgSlugChatIdIndexRoute - "/$orgSlug/settings/chat-sync": typeof AppOrgSlugSettingsChatSyncIndexRoute - "/$orgSlug/settings/integrations": typeof AppOrgSlugSettingsIntegrationsIndexRoute - "/$orgSlug/channels/$channelId/settings/connect": typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute - "/$orgSlug/channels/$channelId/settings/integrations": typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute - "/$orgSlug/channels/$channelId/settings/overview": typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute - "/$orgSlug/chat/$id/files/media": typeof AppOrgSlugChatIdFilesMediaRoute - "/$orgSlug/channels/$channelId/settings": typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute - "/$orgSlug/chat/$id/files": typeof AppOrgSlugChatIdFilesIndexRoute + '/': typeof AppIndexRoute + '/ui': typeof DevUiLayoutRouteWithChildren + '/auth/callback': typeof AuthCallbackRoute + '/auth/desktop-callback': typeof AuthDesktopCallbackRoute + '/auth/desktop-login': typeof AuthDesktopLoginRoute + '/auth/login': typeof AuthLoginRoute + '/join/$slug': typeof JoinSlugRoute + '/onboarding/setup-organization': typeof AppOnboardingSetupOrganizationRoute + '/ui/agent-steps': typeof DevUiAgentStepsRoute + '/dev/embeds/demo': typeof DevEmbedsDemoRoute + '/dev/embeds/github': typeof DevEmbedsGithubRoute + '/dev/embeds/openstatus': typeof DevEmbedsOpenstatusRoute + '/dev/embeds/railway': typeof DevEmbedsRailwayRoute + '/$orgSlug': typeof AppOrgSlugIndexRoute + '/onboarding': typeof AppOnboardingIndexRoute + '/select-organization': typeof AppSelectOrganizationIndexRoute + '/dev/embeds': typeof DevEmbedsIndexRoute + '/$orgSlug/my-settings/desktop': typeof AppOrgSlugMySettingsDesktopRoute + '/$orgSlug/my-settings/linked-accounts': typeof AppOrgSlugMySettingsLinkedAccountsRoute + '/$orgSlug/my-settings/notifications': typeof AppOrgSlugMySettingsNotificationsRoute + '/$orgSlug/my-settings/profile': typeof AppOrgSlugMySettingsProfileRoute + '/$orgSlug/notifications/dms': typeof AppOrgSlugNotificationsDmsRoute + '/$orgSlug/notifications/general': typeof AppOrgSlugNotificationsGeneralRoute + '/$orgSlug/notifications/threads': typeof AppOrgSlugNotificationsThreadsRoute + '/$orgSlug/profile/$userId': typeof AppOrgSlugProfileUserIdRoute + '/$orgSlug/settings/authentication': typeof AppOrgSlugSettingsAuthenticationRoute + '/$orgSlug/settings/connect-invites': typeof AppOrgSlugSettingsConnectInvitesRoute + '/$orgSlug/settings/custom-emojis': typeof AppOrgSlugSettingsCustomEmojisRoute + '/$orgSlug/settings/debug': typeof AppOrgSlugSettingsDebugRoute + '/$orgSlug/settings/invitations': typeof AppOrgSlugSettingsInvitationsRoute + '/$orgSlug/settings/team': typeof AppOrgSlugSettingsTeamRoute + '/$orgSlug/chat': typeof AppOrgSlugChatIndexRoute + '/$orgSlug/my-settings': typeof AppOrgSlugMySettingsIndexRoute + '/$orgSlug/notifications': typeof AppOrgSlugNotificationsIndexRoute + '/$orgSlug/settings': typeof AppOrgSlugSettingsIndexRoute + '/$orgSlug/settings/chat-sync/$connectionId': typeof AppOrgSlugSettingsChatSyncConnectionIdRoute + '/$orgSlug/settings/integrations/$integrationId': typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute + '/$orgSlug/settings/integrations/installed': typeof AppOrgSlugSettingsIntegrationsInstalledRoute + '/$orgSlug/settings/integrations/marketplace': typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute + '/$orgSlug/settings/integrations/your-apps': typeof AppOrgSlugSettingsIntegrationsYourAppsRoute + '/$orgSlug/chat/$id': typeof AppOrgSlugChatIdIndexRoute + '/$orgSlug/settings/chat-sync': typeof AppOrgSlugSettingsChatSyncIndexRoute + '/$orgSlug/settings/integrations': typeof AppOrgSlugSettingsIntegrationsIndexRoute + '/$orgSlug/channels/$channelId/settings/connect': typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute + '/$orgSlug/channels/$channelId/settings/integrations': typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute + '/$orgSlug/channels/$channelId/settings/overview': typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute + '/$orgSlug/chat/$id/files/media': typeof AppOrgSlugChatIdFilesMediaRoute + '/$orgSlug/channels/$channelId/settings': typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute + '/$orgSlug/chat/$id/files': typeof AppOrgSlugChatIdFilesIndexRoute } export interface FileRoutesById { - __root__: typeof rootRouteImport - "/_app": typeof AppLayoutRouteWithChildren - "/_dev": typeof DevLayoutRouteWithChildren - "/_app/$orgSlug": typeof AppOrgSlugLayoutRouteWithChildren - "/_dev/ui": typeof DevUiLayoutRouteWithChildren - "/auth/callback": typeof AuthCallbackRoute - "/auth/desktop-callback": typeof AuthDesktopCallbackRoute - "/auth/desktop-login": typeof AuthDesktopLoginRoute - "/auth/login": typeof AuthLoginRoute - "/join/$slug": typeof JoinSlugRoute - "/_app/": typeof AppIndexRoute - "/_app/$orgSlug/my-settings": typeof AppOrgSlugMySettingsLayoutRouteWithChildren - "/_app/$orgSlug/notifications": typeof AppOrgSlugNotificationsLayoutRouteWithChildren - "/_app/$orgSlug/settings": typeof AppOrgSlugSettingsLayoutRouteWithChildren - "/_app/onboarding/setup-organization": typeof AppOnboardingSetupOrganizationRoute - "/_dev/ui/agent-steps": typeof DevUiAgentStepsRoute - "/dev/embeds/demo": typeof DevEmbedsDemoRoute - "/dev/embeds/github": typeof DevEmbedsGithubRoute - "/dev/embeds/openstatus": typeof DevEmbedsOpenstatusRoute - "/dev/embeds/railway": typeof DevEmbedsRailwayRoute - "/_app/$orgSlug/": typeof AppOrgSlugIndexRoute - "/_app/onboarding/": typeof AppOnboardingIndexRoute - "/_app/select-organization/": typeof AppSelectOrganizationIndexRoute - "/dev/embeds/": typeof DevEmbedsIndexRoute - "/_app/$orgSlug/settings/chat-sync": typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren - "/_app/$orgSlug/settings/integrations": typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren - "/_app/$orgSlug/chat/$id": typeof AppOrgSlugChatIdRouteWithChildren - "/_app/$orgSlug/my-settings/desktop": typeof AppOrgSlugMySettingsDesktopRoute - "/_app/$orgSlug/my-settings/linked-accounts": typeof AppOrgSlugMySettingsLinkedAccountsRoute - "/_app/$orgSlug/my-settings/notifications": typeof AppOrgSlugMySettingsNotificationsRoute - "/_app/$orgSlug/my-settings/profile": typeof AppOrgSlugMySettingsProfileRoute - "/_app/$orgSlug/notifications/dms": typeof AppOrgSlugNotificationsDmsRoute - "/_app/$orgSlug/notifications/general": typeof AppOrgSlugNotificationsGeneralRoute - "/_app/$orgSlug/notifications/threads": typeof AppOrgSlugNotificationsThreadsRoute - "/_app/$orgSlug/profile/$userId": typeof AppOrgSlugProfileUserIdRoute - "/_app/$orgSlug/settings/authentication": typeof AppOrgSlugSettingsAuthenticationRoute - "/_app/$orgSlug/settings/connect-invites": typeof AppOrgSlugSettingsConnectInvitesRoute - "/_app/$orgSlug/settings/custom-emojis": typeof AppOrgSlugSettingsCustomEmojisRoute - "/_app/$orgSlug/settings/debug": typeof AppOrgSlugSettingsDebugRoute - "/_app/$orgSlug/settings/invitations": typeof AppOrgSlugSettingsInvitationsRoute - "/_app/$orgSlug/settings/team": typeof AppOrgSlugSettingsTeamRoute - "/_app/$orgSlug/chat/": typeof AppOrgSlugChatIndexRoute - "/_app/$orgSlug/my-settings/": typeof AppOrgSlugMySettingsIndexRoute - "/_app/$orgSlug/notifications/": typeof AppOrgSlugNotificationsIndexRoute - "/_app/$orgSlug/settings/": typeof AppOrgSlugSettingsIndexRoute - "/_app/$orgSlug/channels/$channelId/settings": typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren - "/_app/$orgSlug/settings/chat-sync/$connectionId": typeof AppOrgSlugSettingsChatSyncConnectionIdRoute - "/_app/$orgSlug/settings/integrations/$integrationId": typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute - "/_app/$orgSlug/settings/integrations/installed": typeof AppOrgSlugSettingsIntegrationsInstalledRoute - "/_app/$orgSlug/settings/integrations/marketplace": typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute - "/_app/$orgSlug/settings/integrations/your-apps": typeof AppOrgSlugSettingsIntegrationsYourAppsRoute - "/_app/$orgSlug/chat/$id/": typeof AppOrgSlugChatIdIndexRoute - "/_app/$orgSlug/settings/chat-sync/": typeof AppOrgSlugSettingsChatSyncIndexRoute - "/_app/$orgSlug/settings/integrations/": typeof AppOrgSlugSettingsIntegrationsIndexRoute - "/_app/$orgSlug/channels/$channelId/settings/connect": typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute - "/_app/$orgSlug/channels/$channelId/settings/integrations": typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute - "/_app/$orgSlug/channels/$channelId/settings/overview": typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute - "/_app/$orgSlug/chat/$id/files/media": typeof AppOrgSlugChatIdFilesMediaRoute - "/_app/$orgSlug/channels/$channelId/settings/": typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute - "/_app/$orgSlug/chat/$id/files/": typeof AppOrgSlugChatIdFilesIndexRoute + __root__: typeof rootRouteImport + '/_app': typeof AppLayoutRouteWithChildren + '/_dev': typeof DevLayoutRouteWithChildren + '/_app/$orgSlug': typeof AppOrgSlugLayoutRouteWithChildren + '/_dev/ui': typeof DevUiLayoutRouteWithChildren + '/auth/callback': typeof AuthCallbackRoute + '/auth/desktop-callback': typeof AuthDesktopCallbackRoute + '/auth/desktop-login': typeof AuthDesktopLoginRoute + '/auth/login': typeof AuthLoginRoute + '/join/$slug': typeof JoinSlugRoute + '/_app/': typeof AppIndexRoute + '/_app/$orgSlug/my-settings': typeof AppOrgSlugMySettingsLayoutRouteWithChildren + '/_app/$orgSlug/notifications': typeof AppOrgSlugNotificationsLayoutRouteWithChildren + '/_app/$orgSlug/settings': typeof AppOrgSlugSettingsLayoutRouteWithChildren + '/_app/onboarding/setup-organization': typeof AppOnboardingSetupOrganizationRoute + '/_dev/ui/agent-steps': typeof DevUiAgentStepsRoute + '/dev/embeds/demo': typeof DevEmbedsDemoRoute + '/dev/embeds/github': typeof DevEmbedsGithubRoute + '/dev/embeds/openstatus': typeof DevEmbedsOpenstatusRoute + '/dev/embeds/railway': typeof DevEmbedsRailwayRoute + '/_app/$orgSlug/': typeof AppOrgSlugIndexRoute + '/_app/onboarding/': typeof AppOnboardingIndexRoute + '/_app/select-organization/': typeof AppSelectOrganizationIndexRoute + '/dev/embeds/': typeof DevEmbedsIndexRoute + '/_app/$orgSlug/settings/chat-sync': typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren + '/_app/$orgSlug/settings/integrations': typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren + '/_app/$orgSlug/chat/$id': typeof AppOrgSlugChatIdRouteWithChildren + '/_app/$orgSlug/my-settings/desktop': typeof AppOrgSlugMySettingsDesktopRoute + '/_app/$orgSlug/my-settings/linked-accounts': typeof AppOrgSlugMySettingsLinkedAccountsRoute + '/_app/$orgSlug/my-settings/notifications': typeof AppOrgSlugMySettingsNotificationsRoute + '/_app/$orgSlug/my-settings/profile': typeof AppOrgSlugMySettingsProfileRoute + '/_app/$orgSlug/notifications/dms': typeof AppOrgSlugNotificationsDmsRoute + '/_app/$orgSlug/notifications/general': typeof AppOrgSlugNotificationsGeneralRoute + '/_app/$orgSlug/notifications/threads': typeof AppOrgSlugNotificationsThreadsRoute + '/_app/$orgSlug/profile/$userId': typeof AppOrgSlugProfileUserIdRoute + '/_app/$orgSlug/settings/authentication': typeof AppOrgSlugSettingsAuthenticationRoute + '/_app/$orgSlug/settings/connect-invites': typeof AppOrgSlugSettingsConnectInvitesRoute + '/_app/$orgSlug/settings/custom-emojis': typeof AppOrgSlugSettingsCustomEmojisRoute + '/_app/$orgSlug/settings/debug': typeof AppOrgSlugSettingsDebugRoute + '/_app/$orgSlug/settings/invitations': typeof AppOrgSlugSettingsInvitationsRoute + '/_app/$orgSlug/settings/team': typeof AppOrgSlugSettingsTeamRoute + '/_app/$orgSlug/chat/': typeof AppOrgSlugChatIndexRoute + '/_app/$orgSlug/my-settings/': typeof AppOrgSlugMySettingsIndexRoute + '/_app/$orgSlug/notifications/': typeof AppOrgSlugNotificationsIndexRoute + '/_app/$orgSlug/settings/': typeof AppOrgSlugSettingsIndexRoute + '/_app/$orgSlug/channels/$channelId/settings': typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren + '/_app/$orgSlug/settings/chat-sync/$connectionId': typeof AppOrgSlugSettingsChatSyncConnectionIdRoute + '/_app/$orgSlug/settings/integrations/$integrationId': typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute + '/_app/$orgSlug/settings/integrations/installed': typeof AppOrgSlugSettingsIntegrationsInstalledRoute + '/_app/$orgSlug/settings/integrations/marketplace': typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute + '/_app/$orgSlug/settings/integrations/your-apps': typeof AppOrgSlugSettingsIntegrationsYourAppsRoute + '/_app/$orgSlug/chat/$id/': typeof AppOrgSlugChatIdIndexRoute + '/_app/$orgSlug/settings/chat-sync/': typeof AppOrgSlugSettingsChatSyncIndexRoute + '/_app/$orgSlug/settings/integrations/': typeof AppOrgSlugSettingsIntegrationsIndexRoute + '/_app/$orgSlug/channels/$channelId/settings/connect': typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute + '/_app/$orgSlug/channels/$channelId/settings/integrations': typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute + '/_app/$orgSlug/channels/$channelId/settings/overview': typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute + '/_app/$orgSlug/chat/$id/files/media': typeof AppOrgSlugChatIdFilesMediaRoute + '/_app/$orgSlug/channels/$channelId/settings/': typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute + '/_app/$orgSlug/chat/$id/files/': typeof AppOrgSlugChatIdFilesIndexRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: - | "/" - | "/$orgSlug" - | "/ui" - | "/auth/callback" - | "/auth/desktop-callback" - | "/auth/desktop-login" - | "/auth/login" - | "/join/$slug" - | "/$orgSlug/my-settings" - | "/$orgSlug/notifications" - | "/$orgSlug/settings" - | "/onboarding/setup-organization" - | "/ui/agent-steps" - | "/dev/embeds/demo" - | "/dev/embeds/github" - | "/dev/embeds/openstatus" - | "/dev/embeds/railway" - | "/$orgSlug/" - | "/onboarding/" - | "/select-organization/" - | "/dev/embeds/" - | "/$orgSlug/settings/chat-sync" - | "/$orgSlug/settings/integrations" - | "/$orgSlug/chat/$id" - | "/$orgSlug/my-settings/desktop" - | "/$orgSlug/my-settings/linked-accounts" - | "/$orgSlug/my-settings/notifications" - | "/$orgSlug/my-settings/profile" - | "/$orgSlug/notifications/dms" - | "/$orgSlug/notifications/general" - | "/$orgSlug/notifications/threads" - | "/$orgSlug/profile/$userId" - | "/$orgSlug/settings/authentication" - | "/$orgSlug/settings/connect-invites" - | "/$orgSlug/settings/custom-emojis" - | "/$orgSlug/settings/debug" - | "/$orgSlug/settings/invitations" - | "/$orgSlug/settings/team" - | "/$orgSlug/chat/" - | "/$orgSlug/my-settings/" - | "/$orgSlug/notifications/" - | "/$orgSlug/settings/" - | "/$orgSlug/channels/$channelId/settings" - | "/$orgSlug/settings/chat-sync/$connectionId" - | "/$orgSlug/settings/integrations/$integrationId" - | "/$orgSlug/settings/integrations/installed" - | "/$orgSlug/settings/integrations/marketplace" - | "/$orgSlug/settings/integrations/your-apps" - | "/$orgSlug/chat/$id/" - | "/$orgSlug/settings/chat-sync/" - | "/$orgSlug/settings/integrations/" - | "/$orgSlug/channels/$channelId/settings/connect" - | "/$orgSlug/channels/$channelId/settings/integrations" - | "/$orgSlug/channels/$channelId/settings/overview" - | "/$orgSlug/chat/$id/files/media" - | "/$orgSlug/channels/$channelId/settings/" - | "/$orgSlug/chat/$id/files/" - fileRoutesByTo: FileRoutesByTo - to: - | "/" - | "/ui" - | "/auth/callback" - | "/auth/desktop-callback" - | "/auth/desktop-login" - | "/auth/login" - | "/join/$slug" - | "/onboarding/setup-organization" - | "/ui/agent-steps" - | "/dev/embeds/demo" - | "/dev/embeds/github" - | "/dev/embeds/openstatus" - | "/dev/embeds/railway" - | "/$orgSlug" - | "/onboarding" - | "/select-organization" - | "/dev/embeds" - | "/$orgSlug/my-settings/desktop" - | "/$orgSlug/my-settings/linked-accounts" - | "/$orgSlug/my-settings/notifications" - | "/$orgSlug/my-settings/profile" - | "/$orgSlug/notifications/dms" - | "/$orgSlug/notifications/general" - | "/$orgSlug/notifications/threads" - | "/$orgSlug/profile/$userId" - | "/$orgSlug/settings/authentication" - | "/$orgSlug/settings/connect-invites" - | "/$orgSlug/settings/custom-emojis" - | "/$orgSlug/settings/debug" - | "/$orgSlug/settings/invitations" - | "/$orgSlug/settings/team" - | "/$orgSlug/chat" - | "/$orgSlug/my-settings" - | "/$orgSlug/notifications" - | "/$orgSlug/settings" - | "/$orgSlug/settings/chat-sync/$connectionId" - | "/$orgSlug/settings/integrations/$integrationId" - | "/$orgSlug/settings/integrations/installed" - | "/$orgSlug/settings/integrations/marketplace" - | "/$orgSlug/settings/integrations/your-apps" - | "/$orgSlug/chat/$id" - | "/$orgSlug/settings/chat-sync" - | "/$orgSlug/settings/integrations" - | "/$orgSlug/channels/$channelId/settings/connect" - | "/$orgSlug/channels/$channelId/settings/integrations" - | "/$orgSlug/channels/$channelId/settings/overview" - | "/$orgSlug/chat/$id/files/media" - | "/$orgSlug/channels/$channelId/settings" - | "/$orgSlug/chat/$id/files" - id: - | "__root__" - | "/_app" - | "/_dev" - | "/_app/$orgSlug" - | "/_dev/ui" - | "/auth/callback" - | "/auth/desktop-callback" - | "/auth/desktop-login" - | "/auth/login" - | "/join/$slug" - | "/_app/" - | "/_app/$orgSlug/my-settings" - | "/_app/$orgSlug/notifications" - | "/_app/$orgSlug/settings" - | "/_app/onboarding/setup-organization" - | "/_dev/ui/agent-steps" - | "/dev/embeds/demo" - | "/dev/embeds/github" - | "/dev/embeds/openstatus" - | "/dev/embeds/railway" - | "/_app/$orgSlug/" - | "/_app/onboarding/" - | "/_app/select-organization/" - | "/dev/embeds/" - | "/_app/$orgSlug/settings/chat-sync" - | "/_app/$orgSlug/settings/integrations" - | "/_app/$orgSlug/chat/$id" - | "/_app/$orgSlug/my-settings/desktop" - | "/_app/$orgSlug/my-settings/linked-accounts" - | "/_app/$orgSlug/my-settings/notifications" - | "/_app/$orgSlug/my-settings/profile" - | "/_app/$orgSlug/notifications/dms" - | "/_app/$orgSlug/notifications/general" - | "/_app/$orgSlug/notifications/threads" - | "/_app/$orgSlug/profile/$userId" - | "/_app/$orgSlug/settings/authentication" - | "/_app/$orgSlug/settings/connect-invites" - | "/_app/$orgSlug/settings/custom-emojis" - | "/_app/$orgSlug/settings/debug" - | "/_app/$orgSlug/settings/invitations" - | "/_app/$orgSlug/settings/team" - | "/_app/$orgSlug/chat/" - | "/_app/$orgSlug/my-settings/" - | "/_app/$orgSlug/notifications/" - | "/_app/$orgSlug/settings/" - | "/_app/$orgSlug/channels/$channelId/settings" - | "/_app/$orgSlug/settings/chat-sync/$connectionId" - | "/_app/$orgSlug/settings/integrations/$integrationId" - | "/_app/$orgSlug/settings/integrations/installed" - | "/_app/$orgSlug/settings/integrations/marketplace" - | "/_app/$orgSlug/settings/integrations/your-apps" - | "/_app/$orgSlug/chat/$id/" - | "/_app/$orgSlug/settings/chat-sync/" - | "/_app/$orgSlug/settings/integrations/" - | "/_app/$orgSlug/channels/$channelId/settings/connect" - | "/_app/$orgSlug/channels/$channelId/settings/integrations" - | "/_app/$orgSlug/channels/$channelId/settings/overview" - | "/_app/$orgSlug/chat/$id/files/media" - | "/_app/$orgSlug/channels/$channelId/settings/" - | "/_app/$orgSlug/chat/$id/files/" - fileRoutesById: FileRoutesById + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/$orgSlug' + | '/ui' + | '/auth/callback' + | '/auth/desktop-callback' + | '/auth/desktop-login' + | '/auth/login' + | '/join/$slug' + | '/$orgSlug/my-settings' + | '/$orgSlug/notifications' + | '/$orgSlug/settings' + | '/onboarding/setup-organization' + | '/ui/agent-steps' + | '/dev/embeds/demo' + | '/dev/embeds/github' + | '/dev/embeds/openstatus' + | '/dev/embeds/railway' + | '/$orgSlug/' + | '/onboarding/' + | '/select-organization/' + | '/dev/embeds/' + | '/$orgSlug/settings/chat-sync' + | '/$orgSlug/settings/integrations' + | '/$orgSlug/chat/$id' + | '/$orgSlug/my-settings/desktop' + | '/$orgSlug/my-settings/linked-accounts' + | '/$orgSlug/my-settings/notifications' + | '/$orgSlug/my-settings/profile' + | '/$orgSlug/notifications/dms' + | '/$orgSlug/notifications/general' + | '/$orgSlug/notifications/threads' + | '/$orgSlug/profile/$userId' + | '/$orgSlug/settings/authentication' + | '/$orgSlug/settings/connect-invites' + | '/$orgSlug/settings/custom-emojis' + | '/$orgSlug/settings/debug' + | '/$orgSlug/settings/invitations' + | '/$orgSlug/settings/team' + | '/$orgSlug/chat/' + | '/$orgSlug/my-settings/' + | '/$orgSlug/notifications/' + | '/$orgSlug/settings/' + | '/$orgSlug/channels/$channelId/settings' + | '/$orgSlug/settings/chat-sync/$connectionId' + | '/$orgSlug/settings/integrations/$integrationId' + | '/$orgSlug/settings/integrations/installed' + | '/$orgSlug/settings/integrations/marketplace' + | '/$orgSlug/settings/integrations/your-apps' + | '/$orgSlug/chat/$id/' + | '/$orgSlug/settings/chat-sync/' + | '/$orgSlug/settings/integrations/' + | '/$orgSlug/channels/$channelId/settings/connect' + | '/$orgSlug/channels/$channelId/settings/integrations' + | '/$orgSlug/channels/$channelId/settings/overview' + | '/$orgSlug/chat/$id/files/media' + | '/$orgSlug/channels/$channelId/settings/' + | '/$orgSlug/chat/$id/files/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/ui' + | '/auth/callback' + | '/auth/desktop-callback' + | '/auth/desktop-login' + | '/auth/login' + | '/join/$slug' + | '/onboarding/setup-organization' + | '/ui/agent-steps' + | '/dev/embeds/demo' + | '/dev/embeds/github' + | '/dev/embeds/openstatus' + | '/dev/embeds/railway' + | '/$orgSlug' + | '/onboarding' + | '/select-organization' + | '/dev/embeds' + | '/$orgSlug/my-settings/desktop' + | '/$orgSlug/my-settings/linked-accounts' + | '/$orgSlug/my-settings/notifications' + | '/$orgSlug/my-settings/profile' + | '/$orgSlug/notifications/dms' + | '/$orgSlug/notifications/general' + | '/$orgSlug/notifications/threads' + | '/$orgSlug/profile/$userId' + | '/$orgSlug/settings/authentication' + | '/$orgSlug/settings/connect-invites' + | '/$orgSlug/settings/custom-emojis' + | '/$orgSlug/settings/debug' + | '/$orgSlug/settings/invitations' + | '/$orgSlug/settings/team' + | '/$orgSlug/chat' + | '/$orgSlug/my-settings' + | '/$orgSlug/notifications' + | '/$orgSlug/settings' + | '/$orgSlug/settings/chat-sync/$connectionId' + | '/$orgSlug/settings/integrations/$integrationId' + | '/$orgSlug/settings/integrations/installed' + | '/$orgSlug/settings/integrations/marketplace' + | '/$orgSlug/settings/integrations/your-apps' + | '/$orgSlug/chat/$id' + | '/$orgSlug/settings/chat-sync' + | '/$orgSlug/settings/integrations' + | '/$orgSlug/channels/$channelId/settings/connect' + | '/$orgSlug/channels/$channelId/settings/integrations' + | '/$orgSlug/channels/$channelId/settings/overview' + | '/$orgSlug/chat/$id/files/media' + | '/$orgSlug/channels/$channelId/settings' + | '/$orgSlug/chat/$id/files' + id: + | '__root__' + | '/_app' + | '/_dev' + | '/_app/$orgSlug' + | '/_dev/ui' + | '/auth/callback' + | '/auth/desktop-callback' + | '/auth/desktop-login' + | '/auth/login' + | '/join/$slug' + | '/_app/' + | '/_app/$orgSlug/my-settings' + | '/_app/$orgSlug/notifications' + | '/_app/$orgSlug/settings' + | '/_app/onboarding/setup-organization' + | '/_dev/ui/agent-steps' + | '/dev/embeds/demo' + | '/dev/embeds/github' + | '/dev/embeds/openstatus' + | '/dev/embeds/railway' + | '/_app/$orgSlug/' + | '/_app/onboarding/' + | '/_app/select-organization/' + | '/dev/embeds/' + | '/_app/$orgSlug/settings/chat-sync' + | '/_app/$orgSlug/settings/integrations' + | '/_app/$orgSlug/chat/$id' + | '/_app/$orgSlug/my-settings/desktop' + | '/_app/$orgSlug/my-settings/linked-accounts' + | '/_app/$orgSlug/my-settings/notifications' + | '/_app/$orgSlug/my-settings/profile' + | '/_app/$orgSlug/notifications/dms' + | '/_app/$orgSlug/notifications/general' + | '/_app/$orgSlug/notifications/threads' + | '/_app/$orgSlug/profile/$userId' + | '/_app/$orgSlug/settings/authentication' + | '/_app/$orgSlug/settings/connect-invites' + | '/_app/$orgSlug/settings/custom-emojis' + | '/_app/$orgSlug/settings/debug' + | '/_app/$orgSlug/settings/invitations' + | '/_app/$orgSlug/settings/team' + | '/_app/$orgSlug/chat/' + | '/_app/$orgSlug/my-settings/' + | '/_app/$orgSlug/notifications/' + | '/_app/$orgSlug/settings/' + | '/_app/$orgSlug/channels/$channelId/settings' + | '/_app/$orgSlug/settings/chat-sync/$connectionId' + | '/_app/$orgSlug/settings/integrations/$integrationId' + | '/_app/$orgSlug/settings/integrations/installed' + | '/_app/$orgSlug/settings/integrations/marketplace' + | '/_app/$orgSlug/settings/integrations/your-apps' + | '/_app/$orgSlug/chat/$id/' + | '/_app/$orgSlug/settings/chat-sync/' + | '/_app/$orgSlug/settings/integrations/' + | '/_app/$orgSlug/channels/$channelId/settings/connect' + | '/_app/$orgSlug/channels/$channelId/settings/integrations' + | '/_app/$orgSlug/channels/$channelId/settings/overview' + | '/_app/$orgSlug/chat/$id/files/media' + | '/_app/$orgSlug/channels/$channelId/settings/' + | '/_app/$orgSlug/chat/$id/files/' + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - AppLayoutRoute: typeof AppLayoutRouteWithChildren - DevLayoutRoute: typeof DevLayoutRouteWithChildren - AuthCallbackRoute: typeof AuthCallbackRoute - AuthDesktopCallbackRoute: typeof AuthDesktopCallbackRoute - AuthDesktopLoginRoute: typeof AuthDesktopLoginRoute - AuthLoginRoute: typeof AuthLoginRoute - JoinSlugRoute: typeof JoinSlugRoute - DevEmbedsDemoRoute: typeof DevEmbedsDemoRoute - DevEmbedsGithubRoute: typeof DevEmbedsGithubRoute - DevEmbedsOpenstatusRoute: typeof DevEmbedsOpenstatusRoute - DevEmbedsRailwayRoute: typeof DevEmbedsRailwayRoute - DevEmbedsIndexRoute: typeof DevEmbedsIndexRoute + AppLayoutRoute: typeof AppLayoutRouteWithChildren + DevLayoutRoute: typeof DevLayoutRouteWithChildren + AuthCallbackRoute: typeof AuthCallbackRoute + AuthDesktopCallbackRoute: typeof AuthDesktopCallbackRoute + AuthDesktopLoginRoute: typeof AuthDesktopLoginRoute + AuthLoginRoute: typeof AuthLoginRoute + JoinSlugRoute: typeof JoinSlugRoute + DevEmbedsDemoRoute: typeof DevEmbedsDemoRoute + DevEmbedsGithubRoute: typeof DevEmbedsGithubRoute + DevEmbedsOpenstatusRoute: typeof DevEmbedsOpenstatusRoute + DevEmbedsRailwayRoute: typeof DevEmbedsRailwayRoute + DevEmbedsIndexRoute: typeof DevEmbedsIndexRoute } -declare module "@tanstack/react-router" { - interface FileRoutesByPath { - "/_dev": { - id: "/_dev" - path: "" - fullPath: "/" - preLoaderRoute: typeof DevLayoutRouteImport - parentRoute: typeof rootRouteImport - } - "/_app": { - id: "/_app" - path: "" - fullPath: "/" - preLoaderRoute: typeof AppLayoutRouteImport - parentRoute: typeof rootRouteImport - } - "/_app/": { - id: "/_app/" - path: "/" - fullPath: "/" - preLoaderRoute: typeof AppIndexRouteImport - parentRoute: typeof AppLayoutRoute - } - "/join/$slug": { - id: "/join/$slug" - path: "/join/$slug" - fullPath: "/join/$slug" - preLoaderRoute: typeof JoinSlugRouteImport - parentRoute: typeof rootRouteImport - } - "/auth/login": { - id: "/auth/login" - path: "/auth/login" - fullPath: "/auth/login" - preLoaderRoute: typeof AuthLoginRouteImport - parentRoute: typeof rootRouteImport - } - "/auth/desktop-login": { - id: "/auth/desktop-login" - path: "/auth/desktop-login" - fullPath: "/auth/desktop-login" - preLoaderRoute: typeof AuthDesktopLoginRouteImport - parentRoute: typeof rootRouteImport - } - "/auth/desktop-callback": { - id: "/auth/desktop-callback" - path: "/auth/desktop-callback" - fullPath: "/auth/desktop-callback" - preLoaderRoute: typeof AuthDesktopCallbackRouteImport - parentRoute: typeof rootRouteImport - } - "/auth/callback": { - id: "/auth/callback" - path: "/auth/callback" - fullPath: "/auth/callback" - preLoaderRoute: typeof AuthCallbackRouteImport - parentRoute: typeof rootRouteImport - } - "/_dev/ui": { - id: "/_dev/ui" - path: "/ui" - fullPath: "/ui" - preLoaderRoute: typeof DevUiLayoutRouteImport - parentRoute: typeof DevLayoutRoute - } - "/_app/$orgSlug": { - id: "/_app/$orgSlug" - path: "/$orgSlug" - fullPath: "/$orgSlug" - preLoaderRoute: typeof AppOrgSlugLayoutRouteImport - parentRoute: typeof AppLayoutRoute - } - "/dev/embeds/": { - id: "/dev/embeds/" - path: "/dev/embeds" - fullPath: "/dev/embeds/" - preLoaderRoute: typeof DevEmbedsIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/_app/select-organization/": { - id: "/_app/select-organization/" - path: "/select-organization" - fullPath: "/select-organization/" - preLoaderRoute: typeof AppSelectOrganizationIndexRouteImport - parentRoute: typeof AppLayoutRoute - } - "/_app/onboarding/": { - id: "/_app/onboarding/" - path: "/onboarding" - fullPath: "/onboarding/" - preLoaderRoute: typeof AppOnboardingIndexRouteImport - parentRoute: typeof AppLayoutRoute - } - "/_app/$orgSlug/": { - id: "/_app/$orgSlug/" - path: "/" - fullPath: "/$orgSlug/" - preLoaderRoute: typeof AppOrgSlugIndexRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - "/dev/embeds/railway": { - id: "/dev/embeds/railway" - path: "/dev/embeds/railway" - fullPath: "/dev/embeds/railway" - preLoaderRoute: typeof DevEmbedsRailwayRouteImport - parentRoute: typeof rootRouteImport - } - "/dev/embeds/openstatus": { - id: "/dev/embeds/openstatus" - path: "/dev/embeds/openstatus" - fullPath: "/dev/embeds/openstatus" - preLoaderRoute: typeof DevEmbedsOpenstatusRouteImport - parentRoute: typeof rootRouteImport - } - "/dev/embeds/github": { - id: "/dev/embeds/github" - path: "/dev/embeds/github" - fullPath: "/dev/embeds/github" - preLoaderRoute: typeof DevEmbedsGithubRouteImport - parentRoute: typeof rootRouteImport - } - "/dev/embeds/demo": { - id: "/dev/embeds/demo" - path: "/dev/embeds/demo" - fullPath: "/dev/embeds/demo" - preLoaderRoute: typeof DevEmbedsDemoRouteImport - parentRoute: typeof rootRouteImport - } - "/_dev/ui/agent-steps": { - id: "/_dev/ui/agent-steps" - path: "/agent-steps" - fullPath: "/ui/agent-steps" - preLoaderRoute: typeof DevUiAgentStepsRouteImport - parentRoute: typeof DevUiLayoutRoute - } - "/_app/onboarding/setup-organization": { - id: "/_app/onboarding/setup-organization" - path: "/onboarding/setup-organization" - fullPath: "/onboarding/setup-organization" - preLoaderRoute: typeof AppOnboardingSetupOrganizationRouteImport - parentRoute: typeof AppLayoutRoute - } - "/_app/$orgSlug/settings": { - id: "/_app/$orgSlug/settings" - path: "/settings" - fullPath: "/$orgSlug/settings" - preLoaderRoute: typeof AppOrgSlugSettingsLayoutRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - "/_app/$orgSlug/notifications": { - id: "/_app/$orgSlug/notifications" - path: "/notifications" - fullPath: "/$orgSlug/notifications" - preLoaderRoute: typeof AppOrgSlugNotificationsLayoutRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - "/_app/$orgSlug/my-settings": { - id: "/_app/$orgSlug/my-settings" - path: "/my-settings" - fullPath: "/$orgSlug/my-settings" - preLoaderRoute: typeof AppOrgSlugMySettingsLayoutRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - "/_app/$orgSlug/settings/": { - id: "/_app/$orgSlug/settings/" - path: "/" - fullPath: "/$orgSlug/settings/" - preLoaderRoute: typeof AppOrgSlugSettingsIndexRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - "/_app/$orgSlug/notifications/": { - id: "/_app/$orgSlug/notifications/" - path: "/" - fullPath: "/$orgSlug/notifications/" - preLoaderRoute: typeof AppOrgSlugNotificationsIndexRouteImport - parentRoute: typeof AppOrgSlugNotificationsLayoutRoute - } - "/_app/$orgSlug/my-settings/": { - id: "/_app/$orgSlug/my-settings/" - path: "/" - fullPath: "/$orgSlug/my-settings/" - preLoaderRoute: typeof AppOrgSlugMySettingsIndexRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - "/_app/$orgSlug/chat/": { - id: "/_app/$orgSlug/chat/" - path: "/chat" - fullPath: "/$orgSlug/chat/" - preLoaderRoute: typeof AppOrgSlugChatIndexRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - "/_app/$orgSlug/settings/team": { - id: "/_app/$orgSlug/settings/team" - path: "/team" - fullPath: "/$orgSlug/settings/team" - preLoaderRoute: typeof AppOrgSlugSettingsTeamRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - "/_app/$orgSlug/settings/invitations": { - id: "/_app/$orgSlug/settings/invitations" - path: "/invitations" - fullPath: "/$orgSlug/settings/invitations" - preLoaderRoute: typeof AppOrgSlugSettingsInvitationsRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - "/_app/$orgSlug/settings/debug": { - id: "/_app/$orgSlug/settings/debug" - path: "/debug" - fullPath: "/$orgSlug/settings/debug" - preLoaderRoute: typeof AppOrgSlugSettingsDebugRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - "/_app/$orgSlug/settings/custom-emojis": { - id: "/_app/$orgSlug/settings/custom-emojis" - path: "/custom-emojis" - fullPath: "/$orgSlug/settings/custom-emojis" - preLoaderRoute: typeof AppOrgSlugSettingsCustomEmojisRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - "/_app/$orgSlug/settings/connect-invites": { - id: "/_app/$orgSlug/settings/connect-invites" - path: "/connect-invites" - fullPath: "/$orgSlug/settings/connect-invites" - preLoaderRoute: typeof AppOrgSlugSettingsConnectInvitesRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - "/_app/$orgSlug/settings/authentication": { - id: "/_app/$orgSlug/settings/authentication" - path: "/authentication" - fullPath: "/$orgSlug/settings/authentication" - preLoaderRoute: typeof AppOrgSlugSettingsAuthenticationRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - "/_app/$orgSlug/profile/$userId": { - id: "/_app/$orgSlug/profile/$userId" - path: "/profile/$userId" - fullPath: "/$orgSlug/profile/$userId" - preLoaderRoute: typeof AppOrgSlugProfileUserIdRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - "/_app/$orgSlug/notifications/threads": { - id: "/_app/$orgSlug/notifications/threads" - path: "/threads" - fullPath: "/$orgSlug/notifications/threads" - preLoaderRoute: typeof AppOrgSlugNotificationsThreadsRouteImport - parentRoute: typeof AppOrgSlugNotificationsLayoutRoute - } - "/_app/$orgSlug/notifications/general": { - id: "/_app/$orgSlug/notifications/general" - path: "/general" - fullPath: "/$orgSlug/notifications/general" - preLoaderRoute: typeof AppOrgSlugNotificationsGeneralRouteImport - parentRoute: typeof AppOrgSlugNotificationsLayoutRoute - } - "/_app/$orgSlug/notifications/dms": { - id: "/_app/$orgSlug/notifications/dms" - path: "/dms" - fullPath: "/$orgSlug/notifications/dms" - preLoaderRoute: typeof AppOrgSlugNotificationsDmsRouteImport - parentRoute: typeof AppOrgSlugNotificationsLayoutRoute - } - "/_app/$orgSlug/my-settings/profile": { - id: "/_app/$orgSlug/my-settings/profile" - path: "/profile" - fullPath: "/$orgSlug/my-settings/profile" - preLoaderRoute: typeof AppOrgSlugMySettingsProfileRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - "/_app/$orgSlug/my-settings/notifications": { - id: "/_app/$orgSlug/my-settings/notifications" - path: "/notifications" - fullPath: "/$orgSlug/my-settings/notifications" - preLoaderRoute: typeof AppOrgSlugMySettingsNotificationsRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - "/_app/$orgSlug/my-settings/linked-accounts": { - id: "/_app/$orgSlug/my-settings/linked-accounts" - path: "/linked-accounts" - fullPath: "/$orgSlug/my-settings/linked-accounts" - preLoaderRoute: typeof AppOrgSlugMySettingsLinkedAccountsRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - "/_app/$orgSlug/my-settings/desktop": { - id: "/_app/$orgSlug/my-settings/desktop" - path: "/desktop" - fullPath: "/$orgSlug/my-settings/desktop" - preLoaderRoute: typeof AppOrgSlugMySettingsDesktopRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - "/_app/$orgSlug/chat/$id": { - id: "/_app/$orgSlug/chat/$id" - path: "/chat/$id" - fullPath: "/$orgSlug/chat/$id" - preLoaderRoute: typeof AppOrgSlugChatIdRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - "/_app/$orgSlug/settings/integrations": { - id: "/_app/$orgSlug/settings/integrations" - path: "/integrations" - fullPath: "/$orgSlug/settings/integrations" - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - "/_app/$orgSlug/settings/chat-sync": { - id: "/_app/$orgSlug/settings/chat-sync" - path: "/chat-sync" - fullPath: "/$orgSlug/settings/chat-sync" - preLoaderRoute: typeof AppOrgSlugSettingsChatSyncLayoutRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - "/_app/$orgSlug/settings/integrations/": { - id: "/_app/$orgSlug/settings/integrations/" - path: "/" - fullPath: "/$orgSlug/settings/integrations/" - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsIndexRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - "/_app/$orgSlug/settings/chat-sync/": { - id: "/_app/$orgSlug/settings/chat-sync/" - path: "/" - fullPath: "/$orgSlug/settings/chat-sync/" - preLoaderRoute: typeof AppOrgSlugSettingsChatSyncIndexRouteImport - parentRoute: typeof AppOrgSlugSettingsChatSyncLayoutRoute - } - "/_app/$orgSlug/chat/$id/": { - id: "/_app/$orgSlug/chat/$id/" - path: "/" - fullPath: "/$orgSlug/chat/$id/" - preLoaderRoute: typeof AppOrgSlugChatIdIndexRouteImport - parentRoute: typeof AppOrgSlugChatIdRoute - } - "/_app/$orgSlug/settings/integrations/your-apps": { - id: "/_app/$orgSlug/settings/integrations/your-apps" - path: "/your-apps" - fullPath: "/$orgSlug/settings/integrations/your-apps" - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsYourAppsRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - "/_app/$orgSlug/settings/integrations/marketplace": { - id: "/_app/$orgSlug/settings/integrations/marketplace" - path: "/marketplace" - fullPath: "/$orgSlug/settings/integrations/marketplace" - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsMarketplaceRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - "/_app/$orgSlug/settings/integrations/installed": { - id: "/_app/$orgSlug/settings/integrations/installed" - path: "/installed" - fullPath: "/$orgSlug/settings/integrations/installed" - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsInstalledRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - "/_app/$orgSlug/settings/integrations/$integrationId": { - id: "/_app/$orgSlug/settings/integrations/$integrationId" - path: "/$integrationId" - fullPath: "/$orgSlug/settings/integrations/$integrationId" - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - "/_app/$orgSlug/settings/chat-sync/$connectionId": { - id: "/_app/$orgSlug/settings/chat-sync/$connectionId" - path: "/$connectionId" - fullPath: "/$orgSlug/settings/chat-sync/$connectionId" - preLoaderRoute: typeof AppOrgSlugSettingsChatSyncConnectionIdRouteImport - parentRoute: typeof AppOrgSlugSettingsChatSyncLayoutRoute - } - "/_app/$orgSlug/channels/$channelId/settings": { - id: "/_app/$orgSlug/channels/$channelId/settings" - path: "/channels/$channelId/settings" - fullPath: "/$orgSlug/channels/$channelId/settings" - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - "/_app/$orgSlug/chat/$id/files/": { - id: "/_app/$orgSlug/chat/$id/files/" - path: "/files" - fullPath: "/$orgSlug/chat/$id/files/" - preLoaderRoute: typeof AppOrgSlugChatIdFilesIndexRouteImport - parentRoute: typeof AppOrgSlugChatIdRoute - } - "/_app/$orgSlug/channels/$channelId/settings/": { - id: "/_app/$orgSlug/channels/$channelId/settings/" - path: "/" - fullPath: "/$orgSlug/channels/$channelId/settings/" - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsIndexRouteImport - parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute - } - "/_app/$orgSlug/chat/$id/files/media": { - id: "/_app/$orgSlug/chat/$id/files/media" - path: "/files/media" - fullPath: "/$orgSlug/chat/$id/files/media" - preLoaderRoute: typeof AppOrgSlugChatIdFilesMediaRouteImport - parentRoute: typeof AppOrgSlugChatIdRoute - } - "/_app/$orgSlug/channels/$channelId/settings/overview": { - id: "/_app/$orgSlug/channels/$channelId/settings/overview" - path: "/overview" - fullPath: "/$orgSlug/channels/$channelId/settings/overview" - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport - parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute - } - "/_app/$orgSlug/channels/$channelId/settings/integrations": { - id: "/_app/$orgSlug/channels/$channelId/settings/integrations" - path: "/integrations" - fullPath: "/$orgSlug/channels/$channelId/settings/integrations" - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport - parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute - } - "/_app/$orgSlug/channels/$channelId/settings/connect": { - id: "/_app/$orgSlug/channels/$channelId/settings/connect" - path: "/connect" - fullPath: "/$orgSlug/channels/$channelId/settings/connect" - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsConnectRouteImport - parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute - } - } +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/_dev': { + id: '/_dev' + path: '' + fullPath: '/' + preLoaderRoute: typeof DevLayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/_app': { + id: '/_app' + path: '' + fullPath: '/' + preLoaderRoute: typeof AppLayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/_app/': { + id: '/_app/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof AppIndexRouteImport + parentRoute: typeof AppLayoutRoute + } + '/join/$slug': { + id: '/join/$slug' + path: '/join/$slug' + fullPath: '/join/$slug' + preLoaderRoute: typeof JoinSlugRouteImport + parentRoute: typeof rootRouteImport + } + '/auth/login': { + id: '/auth/login' + path: '/auth/login' + fullPath: '/auth/login' + preLoaderRoute: typeof AuthLoginRouteImport + parentRoute: typeof rootRouteImport + } + '/auth/desktop-login': { + id: '/auth/desktop-login' + path: '/auth/desktop-login' + fullPath: '/auth/desktop-login' + preLoaderRoute: typeof AuthDesktopLoginRouteImport + parentRoute: typeof rootRouteImport + } + '/auth/desktop-callback': { + id: '/auth/desktop-callback' + path: '/auth/desktop-callback' + fullPath: '/auth/desktop-callback' + preLoaderRoute: typeof AuthDesktopCallbackRouteImport + parentRoute: typeof rootRouteImport + } + '/auth/callback': { + id: '/auth/callback' + path: '/auth/callback' + fullPath: '/auth/callback' + preLoaderRoute: typeof AuthCallbackRouteImport + parentRoute: typeof rootRouteImport + } + '/_dev/ui': { + id: '/_dev/ui' + path: '/ui' + fullPath: '/ui' + preLoaderRoute: typeof DevUiLayoutRouteImport + parentRoute: typeof DevLayoutRoute + } + '/_app/$orgSlug': { + id: '/_app/$orgSlug' + path: '/$orgSlug' + fullPath: '/$orgSlug' + preLoaderRoute: typeof AppOrgSlugLayoutRouteImport + parentRoute: typeof AppLayoutRoute + } + '/dev/embeds/': { + id: '/dev/embeds/' + path: '/dev/embeds' + fullPath: '/dev/embeds/' + preLoaderRoute: typeof DevEmbedsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/_app/select-organization/': { + id: '/_app/select-organization/' + path: '/select-organization' + fullPath: '/select-organization/' + preLoaderRoute: typeof AppSelectOrganizationIndexRouteImport + parentRoute: typeof AppLayoutRoute + } + '/_app/onboarding/': { + id: '/_app/onboarding/' + path: '/onboarding' + fullPath: '/onboarding/' + preLoaderRoute: typeof AppOnboardingIndexRouteImport + parentRoute: typeof AppLayoutRoute + } + '/_app/$orgSlug/': { + id: '/_app/$orgSlug/' + path: '/' + fullPath: '/$orgSlug/' + preLoaderRoute: typeof AppOrgSlugIndexRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + '/dev/embeds/railway': { + id: '/dev/embeds/railway' + path: '/dev/embeds/railway' + fullPath: '/dev/embeds/railway' + preLoaderRoute: typeof DevEmbedsRailwayRouteImport + parentRoute: typeof rootRouteImport + } + '/dev/embeds/openstatus': { + id: '/dev/embeds/openstatus' + path: '/dev/embeds/openstatus' + fullPath: '/dev/embeds/openstatus' + preLoaderRoute: typeof DevEmbedsOpenstatusRouteImport + parentRoute: typeof rootRouteImport + } + '/dev/embeds/github': { + id: '/dev/embeds/github' + path: '/dev/embeds/github' + fullPath: '/dev/embeds/github' + preLoaderRoute: typeof DevEmbedsGithubRouteImport + parentRoute: typeof rootRouteImport + } + '/dev/embeds/demo': { + id: '/dev/embeds/demo' + path: '/dev/embeds/demo' + fullPath: '/dev/embeds/demo' + preLoaderRoute: typeof DevEmbedsDemoRouteImport + parentRoute: typeof rootRouteImport + } + '/_dev/ui/agent-steps': { + id: '/_dev/ui/agent-steps' + path: '/agent-steps' + fullPath: '/ui/agent-steps' + preLoaderRoute: typeof DevUiAgentStepsRouteImport + parentRoute: typeof DevUiLayoutRoute + } + '/_app/onboarding/setup-organization': { + id: '/_app/onboarding/setup-organization' + path: '/onboarding/setup-organization' + fullPath: '/onboarding/setup-organization' + preLoaderRoute: typeof AppOnboardingSetupOrganizationRouteImport + parentRoute: typeof AppLayoutRoute + } + '/_app/$orgSlug/settings': { + id: '/_app/$orgSlug/settings' + path: '/settings' + fullPath: '/$orgSlug/settings' + preLoaderRoute: typeof AppOrgSlugSettingsLayoutRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + '/_app/$orgSlug/notifications': { + id: '/_app/$orgSlug/notifications' + path: '/notifications' + fullPath: '/$orgSlug/notifications' + preLoaderRoute: typeof AppOrgSlugNotificationsLayoutRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + '/_app/$orgSlug/my-settings': { + id: '/_app/$orgSlug/my-settings' + path: '/my-settings' + fullPath: '/$orgSlug/my-settings' + preLoaderRoute: typeof AppOrgSlugMySettingsLayoutRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + '/_app/$orgSlug/settings/': { + id: '/_app/$orgSlug/settings/' + path: '/' + fullPath: '/$orgSlug/settings/' + preLoaderRoute: typeof AppOrgSlugSettingsIndexRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + '/_app/$orgSlug/notifications/': { + id: '/_app/$orgSlug/notifications/' + path: '/' + fullPath: '/$orgSlug/notifications/' + preLoaderRoute: typeof AppOrgSlugNotificationsIndexRouteImport + parentRoute: typeof AppOrgSlugNotificationsLayoutRoute + } + '/_app/$orgSlug/my-settings/': { + id: '/_app/$orgSlug/my-settings/' + path: '/' + fullPath: '/$orgSlug/my-settings/' + preLoaderRoute: typeof AppOrgSlugMySettingsIndexRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + '/_app/$orgSlug/chat/': { + id: '/_app/$orgSlug/chat/' + path: '/chat' + fullPath: '/$orgSlug/chat/' + preLoaderRoute: typeof AppOrgSlugChatIndexRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + '/_app/$orgSlug/settings/team': { + id: '/_app/$orgSlug/settings/team' + path: '/team' + fullPath: '/$orgSlug/settings/team' + preLoaderRoute: typeof AppOrgSlugSettingsTeamRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + '/_app/$orgSlug/settings/invitations': { + id: '/_app/$orgSlug/settings/invitations' + path: '/invitations' + fullPath: '/$orgSlug/settings/invitations' + preLoaderRoute: typeof AppOrgSlugSettingsInvitationsRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + '/_app/$orgSlug/settings/debug': { + id: '/_app/$orgSlug/settings/debug' + path: '/debug' + fullPath: '/$orgSlug/settings/debug' + preLoaderRoute: typeof AppOrgSlugSettingsDebugRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + '/_app/$orgSlug/settings/custom-emojis': { + id: '/_app/$orgSlug/settings/custom-emojis' + path: '/custom-emojis' + fullPath: '/$orgSlug/settings/custom-emojis' + preLoaderRoute: typeof AppOrgSlugSettingsCustomEmojisRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + '/_app/$orgSlug/settings/connect-invites': { + id: '/_app/$orgSlug/settings/connect-invites' + path: '/connect-invites' + fullPath: '/$orgSlug/settings/connect-invites' + preLoaderRoute: typeof AppOrgSlugSettingsConnectInvitesRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + '/_app/$orgSlug/settings/authentication': { + id: '/_app/$orgSlug/settings/authentication' + path: '/authentication' + fullPath: '/$orgSlug/settings/authentication' + preLoaderRoute: typeof AppOrgSlugSettingsAuthenticationRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + '/_app/$orgSlug/profile/$userId': { + id: '/_app/$orgSlug/profile/$userId' + path: '/profile/$userId' + fullPath: '/$orgSlug/profile/$userId' + preLoaderRoute: typeof AppOrgSlugProfileUserIdRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + '/_app/$orgSlug/notifications/threads': { + id: '/_app/$orgSlug/notifications/threads' + path: '/threads' + fullPath: '/$orgSlug/notifications/threads' + preLoaderRoute: typeof AppOrgSlugNotificationsThreadsRouteImport + parentRoute: typeof AppOrgSlugNotificationsLayoutRoute + } + '/_app/$orgSlug/notifications/general': { + id: '/_app/$orgSlug/notifications/general' + path: '/general' + fullPath: '/$orgSlug/notifications/general' + preLoaderRoute: typeof AppOrgSlugNotificationsGeneralRouteImport + parentRoute: typeof AppOrgSlugNotificationsLayoutRoute + } + '/_app/$orgSlug/notifications/dms': { + id: '/_app/$orgSlug/notifications/dms' + path: '/dms' + fullPath: '/$orgSlug/notifications/dms' + preLoaderRoute: typeof AppOrgSlugNotificationsDmsRouteImport + parentRoute: typeof AppOrgSlugNotificationsLayoutRoute + } + '/_app/$orgSlug/my-settings/profile': { + id: '/_app/$orgSlug/my-settings/profile' + path: '/profile' + fullPath: '/$orgSlug/my-settings/profile' + preLoaderRoute: typeof AppOrgSlugMySettingsProfileRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + '/_app/$orgSlug/my-settings/notifications': { + id: '/_app/$orgSlug/my-settings/notifications' + path: '/notifications' + fullPath: '/$orgSlug/my-settings/notifications' + preLoaderRoute: typeof AppOrgSlugMySettingsNotificationsRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + '/_app/$orgSlug/my-settings/linked-accounts': { + id: '/_app/$orgSlug/my-settings/linked-accounts' + path: '/linked-accounts' + fullPath: '/$orgSlug/my-settings/linked-accounts' + preLoaderRoute: typeof AppOrgSlugMySettingsLinkedAccountsRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + '/_app/$orgSlug/my-settings/desktop': { + id: '/_app/$orgSlug/my-settings/desktop' + path: '/desktop' + fullPath: '/$orgSlug/my-settings/desktop' + preLoaderRoute: typeof AppOrgSlugMySettingsDesktopRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + '/_app/$orgSlug/chat/$id': { + id: '/_app/$orgSlug/chat/$id' + path: '/chat/$id' + fullPath: '/$orgSlug/chat/$id' + preLoaderRoute: typeof AppOrgSlugChatIdRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + '/_app/$orgSlug/settings/integrations': { + id: '/_app/$orgSlug/settings/integrations' + path: '/integrations' + fullPath: '/$orgSlug/settings/integrations' + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + '/_app/$orgSlug/settings/chat-sync': { + id: '/_app/$orgSlug/settings/chat-sync' + path: '/chat-sync' + fullPath: '/$orgSlug/settings/chat-sync' + preLoaderRoute: typeof AppOrgSlugSettingsChatSyncLayoutRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + '/_app/$orgSlug/settings/integrations/': { + id: '/_app/$orgSlug/settings/integrations/' + path: '/' + fullPath: '/$orgSlug/settings/integrations/' + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsIndexRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + '/_app/$orgSlug/settings/chat-sync/': { + id: '/_app/$orgSlug/settings/chat-sync/' + path: '/' + fullPath: '/$orgSlug/settings/chat-sync/' + preLoaderRoute: typeof AppOrgSlugSettingsChatSyncIndexRouteImport + parentRoute: typeof AppOrgSlugSettingsChatSyncLayoutRoute + } + '/_app/$orgSlug/chat/$id/': { + id: '/_app/$orgSlug/chat/$id/' + path: '/' + fullPath: '/$orgSlug/chat/$id/' + preLoaderRoute: typeof AppOrgSlugChatIdIndexRouteImport + parentRoute: typeof AppOrgSlugChatIdRoute + } + '/_app/$orgSlug/settings/integrations/your-apps': { + id: '/_app/$orgSlug/settings/integrations/your-apps' + path: '/your-apps' + fullPath: '/$orgSlug/settings/integrations/your-apps' + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsYourAppsRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + '/_app/$orgSlug/settings/integrations/marketplace': { + id: '/_app/$orgSlug/settings/integrations/marketplace' + path: '/marketplace' + fullPath: '/$orgSlug/settings/integrations/marketplace' + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsMarketplaceRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + '/_app/$orgSlug/settings/integrations/installed': { + id: '/_app/$orgSlug/settings/integrations/installed' + path: '/installed' + fullPath: '/$orgSlug/settings/integrations/installed' + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsInstalledRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + '/_app/$orgSlug/settings/integrations/$integrationId': { + id: '/_app/$orgSlug/settings/integrations/$integrationId' + path: '/$integrationId' + fullPath: '/$orgSlug/settings/integrations/$integrationId' + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + '/_app/$orgSlug/settings/chat-sync/$connectionId': { + id: '/_app/$orgSlug/settings/chat-sync/$connectionId' + path: '/$connectionId' + fullPath: '/$orgSlug/settings/chat-sync/$connectionId' + preLoaderRoute: typeof AppOrgSlugSettingsChatSyncConnectionIdRouteImport + parentRoute: typeof AppOrgSlugSettingsChatSyncLayoutRoute + } + '/_app/$orgSlug/channels/$channelId/settings': { + id: '/_app/$orgSlug/channels/$channelId/settings' + path: '/channels/$channelId/settings' + fullPath: '/$orgSlug/channels/$channelId/settings' + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + '/_app/$orgSlug/chat/$id/files/': { + id: '/_app/$orgSlug/chat/$id/files/' + path: '/files' + fullPath: '/$orgSlug/chat/$id/files/' + preLoaderRoute: typeof AppOrgSlugChatIdFilesIndexRouteImport + parentRoute: typeof AppOrgSlugChatIdRoute + } + '/_app/$orgSlug/channels/$channelId/settings/': { + id: '/_app/$orgSlug/channels/$channelId/settings/' + path: '/' + fullPath: '/$orgSlug/channels/$channelId/settings/' + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsIndexRouteImport + parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute + } + '/_app/$orgSlug/chat/$id/files/media': { + id: '/_app/$orgSlug/chat/$id/files/media' + path: '/files/media' + fullPath: '/$orgSlug/chat/$id/files/media' + preLoaderRoute: typeof AppOrgSlugChatIdFilesMediaRouteImport + parentRoute: typeof AppOrgSlugChatIdRoute + } + '/_app/$orgSlug/channels/$channelId/settings/overview': { + id: '/_app/$orgSlug/channels/$channelId/settings/overview' + path: '/overview' + fullPath: '/$orgSlug/channels/$channelId/settings/overview' + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport + parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute + } + '/_app/$orgSlug/channels/$channelId/settings/integrations': { + id: '/_app/$orgSlug/channels/$channelId/settings/integrations' + path: '/integrations' + fullPath: '/$orgSlug/channels/$channelId/settings/integrations' + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport + parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute + } + '/_app/$orgSlug/channels/$channelId/settings/connect': { + id: '/_app/$orgSlug/channels/$channelId/settings/connect' + path: '/connect' + fullPath: '/$orgSlug/channels/$channelId/settings/connect' + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsConnectRouteImport + parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute + } + } } interface AppOrgSlugMySettingsLayoutRouteChildren { - AppOrgSlugMySettingsDesktopRoute: typeof AppOrgSlugMySettingsDesktopRoute - AppOrgSlugMySettingsLinkedAccountsRoute: typeof AppOrgSlugMySettingsLinkedAccountsRoute - AppOrgSlugMySettingsNotificationsRoute: typeof AppOrgSlugMySettingsNotificationsRoute - AppOrgSlugMySettingsProfileRoute: typeof AppOrgSlugMySettingsProfileRoute - AppOrgSlugMySettingsIndexRoute: typeof AppOrgSlugMySettingsIndexRoute + AppOrgSlugMySettingsDesktopRoute: typeof AppOrgSlugMySettingsDesktopRoute + AppOrgSlugMySettingsLinkedAccountsRoute: typeof AppOrgSlugMySettingsLinkedAccountsRoute + AppOrgSlugMySettingsNotificationsRoute: typeof AppOrgSlugMySettingsNotificationsRoute + AppOrgSlugMySettingsProfileRoute: typeof AppOrgSlugMySettingsProfileRoute + AppOrgSlugMySettingsIndexRoute: typeof AppOrgSlugMySettingsIndexRoute } -const AppOrgSlugMySettingsLayoutRouteChildren: AppOrgSlugMySettingsLayoutRouteChildren = { - AppOrgSlugMySettingsDesktopRoute: AppOrgSlugMySettingsDesktopRoute, - AppOrgSlugMySettingsLinkedAccountsRoute: AppOrgSlugMySettingsLinkedAccountsRoute, - AppOrgSlugMySettingsNotificationsRoute: AppOrgSlugMySettingsNotificationsRoute, - AppOrgSlugMySettingsProfileRoute: AppOrgSlugMySettingsProfileRoute, - AppOrgSlugMySettingsIndexRoute: AppOrgSlugMySettingsIndexRoute, -} +const AppOrgSlugMySettingsLayoutRouteChildren: AppOrgSlugMySettingsLayoutRouteChildren = + { + AppOrgSlugMySettingsDesktopRoute: AppOrgSlugMySettingsDesktopRoute, + AppOrgSlugMySettingsLinkedAccountsRoute: + AppOrgSlugMySettingsLinkedAccountsRoute, + AppOrgSlugMySettingsNotificationsRoute: + AppOrgSlugMySettingsNotificationsRoute, + AppOrgSlugMySettingsProfileRoute: AppOrgSlugMySettingsProfileRoute, + AppOrgSlugMySettingsIndexRoute: AppOrgSlugMySettingsIndexRoute, + } -const AppOrgSlugMySettingsLayoutRouteWithChildren = AppOrgSlugMySettingsLayoutRoute._addFileChildren( - AppOrgSlugMySettingsLayoutRouteChildren, -) +const AppOrgSlugMySettingsLayoutRouteWithChildren = + AppOrgSlugMySettingsLayoutRoute._addFileChildren( + AppOrgSlugMySettingsLayoutRouteChildren, + ) interface AppOrgSlugNotificationsLayoutRouteChildren { - AppOrgSlugNotificationsDmsRoute: typeof AppOrgSlugNotificationsDmsRoute - AppOrgSlugNotificationsGeneralRoute: typeof AppOrgSlugNotificationsGeneralRoute - AppOrgSlugNotificationsThreadsRoute: typeof AppOrgSlugNotificationsThreadsRoute - AppOrgSlugNotificationsIndexRoute: typeof AppOrgSlugNotificationsIndexRoute + AppOrgSlugNotificationsDmsRoute: typeof AppOrgSlugNotificationsDmsRoute + AppOrgSlugNotificationsGeneralRoute: typeof AppOrgSlugNotificationsGeneralRoute + AppOrgSlugNotificationsThreadsRoute: typeof AppOrgSlugNotificationsThreadsRoute + AppOrgSlugNotificationsIndexRoute: typeof AppOrgSlugNotificationsIndexRoute } -const AppOrgSlugNotificationsLayoutRouteChildren: AppOrgSlugNotificationsLayoutRouteChildren = { - AppOrgSlugNotificationsDmsRoute: AppOrgSlugNotificationsDmsRoute, - AppOrgSlugNotificationsGeneralRoute: AppOrgSlugNotificationsGeneralRoute, - AppOrgSlugNotificationsThreadsRoute: AppOrgSlugNotificationsThreadsRoute, - AppOrgSlugNotificationsIndexRoute: AppOrgSlugNotificationsIndexRoute, -} +const AppOrgSlugNotificationsLayoutRouteChildren: AppOrgSlugNotificationsLayoutRouteChildren = + { + AppOrgSlugNotificationsDmsRoute: AppOrgSlugNotificationsDmsRoute, + AppOrgSlugNotificationsGeneralRoute: AppOrgSlugNotificationsGeneralRoute, + AppOrgSlugNotificationsThreadsRoute: AppOrgSlugNotificationsThreadsRoute, + AppOrgSlugNotificationsIndexRoute: AppOrgSlugNotificationsIndexRoute, + } -const AppOrgSlugNotificationsLayoutRouteWithChildren = AppOrgSlugNotificationsLayoutRoute._addFileChildren( - AppOrgSlugNotificationsLayoutRouteChildren, -) +const AppOrgSlugNotificationsLayoutRouteWithChildren = + AppOrgSlugNotificationsLayoutRoute._addFileChildren( + AppOrgSlugNotificationsLayoutRouteChildren, + ) interface AppOrgSlugSettingsChatSyncLayoutRouteChildren { - AppOrgSlugSettingsChatSyncConnectionIdRoute: typeof AppOrgSlugSettingsChatSyncConnectionIdRoute - AppOrgSlugSettingsChatSyncIndexRoute: typeof AppOrgSlugSettingsChatSyncIndexRoute + AppOrgSlugSettingsChatSyncConnectionIdRoute: typeof AppOrgSlugSettingsChatSyncConnectionIdRoute + AppOrgSlugSettingsChatSyncIndexRoute: typeof AppOrgSlugSettingsChatSyncIndexRoute } -const AppOrgSlugSettingsChatSyncLayoutRouteChildren: AppOrgSlugSettingsChatSyncLayoutRouteChildren = { - AppOrgSlugSettingsChatSyncConnectionIdRoute: AppOrgSlugSettingsChatSyncConnectionIdRoute, - AppOrgSlugSettingsChatSyncIndexRoute: AppOrgSlugSettingsChatSyncIndexRoute, -} +const AppOrgSlugSettingsChatSyncLayoutRouteChildren: AppOrgSlugSettingsChatSyncLayoutRouteChildren = + { + AppOrgSlugSettingsChatSyncConnectionIdRoute: + AppOrgSlugSettingsChatSyncConnectionIdRoute, + AppOrgSlugSettingsChatSyncIndexRoute: AppOrgSlugSettingsChatSyncIndexRoute, + } const AppOrgSlugSettingsChatSyncLayoutRouteWithChildren = - AppOrgSlugSettingsChatSyncLayoutRoute._addFileChildren(AppOrgSlugSettingsChatSyncLayoutRouteChildren) + AppOrgSlugSettingsChatSyncLayoutRoute._addFileChildren( + AppOrgSlugSettingsChatSyncLayoutRouteChildren, + ) interface AppOrgSlugSettingsIntegrationsLayoutRouteChildren { - AppOrgSlugSettingsIntegrationsIntegrationIdRoute: typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute - AppOrgSlugSettingsIntegrationsInstalledRoute: typeof AppOrgSlugSettingsIntegrationsInstalledRoute - AppOrgSlugSettingsIntegrationsMarketplaceRoute: typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute - AppOrgSlugSettingsIntegrationsYourAppsRoute: typeof AppOrgSlugSettingsIntegrationsYourAppsRoute - AppOrgSlugSettingsIntegrationsIndexRoute: typeof AppOrgSlugSettingsIntegrationsIndexRoute + AppOrgSlugSettingsIntegrationsIntegrationIdRoute: typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute + AppOrgSlugSettingsIntegrationsInstalledRoute: typeof AppOrgSlugSettingsIntegrationsInstalledRoute + AppOrgSlugSettingsIntegrationsMarketplaceRoute: typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute + AppOrgSlugSettingsIntegrationsYourAppsRoute: typeof AppOrgSlugSettingsIntegrationsYourAppsRoute + AppOrgSlugSettingsIntegrationsIndexRoute: typeof AppOrgSlugSettingsIntegrationsIndexRoute } -const AppOrgSlugSettingsIntegrationsLayoutRouteChildren: AppOrgSlugSettingsIntegrationsLayoutRouteChildren = { - AppOrgSlugSettingsIntegrationsIntegrationIdRoute: AppOrgSlugSettingsIntegrationsIntegrationIdRoute, - AppOrgSlugSettingsIntegrationsInstalledRoute: AppOrgSlugSettingsIntegrationsInstalledRoute, - AppOrgSlugSettingsIntegrationsMarketplaceRoute: AppOrgSlugSettingsIntegrationsMarketplaceRoute, - AppOrgSlugSettingsIntegrationsYourAppsRoute: AppOrgSlugSettingsIntegrationsYourAppsRoute, - AppOrgSlugSettingsIntegrationsIndexRoute: AppOrgSlugSettingsIntegrationsIndexRoute, -} +const AppOrgSlugSettingsIntegrationsLayoutRouteChildren: AppOrgSlugSettingsIntegrationsLayoutRouteChildren = + { + AppOrgSlugSettingsIntegrationsIntegrationIdRoute: + AppOrgSlugSettingsIntegrationsIntegrationIdRoute, + AppOrgSlugSettingsIntegrationsInstalledRoute: + AppOrgSlugSettingsIntegrationsInstalledRoute, + AppOrgSlugSettingsIntegrationsMarketplaceRoute: + AppOrgSlugSettingsIntegrationsMarketplaceRoute, + AppOrgSlugSettingsIntegrationsYourAppsRoute: + AppOrgSlugSettingsIntegrationsYourAppsRoute, + AppOrgSlugSettingsIntegrationsIndexRoute: + AppOrgSlugSettingsIntegrationsIndexRoute, + } const AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren = - AppOrgSlugSettingsIntegrationsLayoutRoute._addFileChildren( - AppOrgSlugSettingsIntegrationsLayoutRouteChildren, - ) + AppOrgSlugSettingsIntegrationsLayoutRoute._addFileChildren( + AppOrgSlugSettingsIntegrationsLayoutRouteChildren, + ) interface AppOrgSlugSettingsLayoutRouteChildren { - AppOrgSlugSettingsChatSyncLayoutRoute: typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren - AppOrgSlugSettingsIntegrationsLayoutRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren - AppOrgSlugSettingsAuthenticationRoute: typeof AppOrgSlugSettingsAuthenticationRoute - AppOrgSlugSettingsConnectInvitesRoute: typeof AppOrgSlugSettingsConnectInvitesRoute - AppOrgSlugSettingsCustomEmojisRoute: typeof AppOrgSlugSettingsCustomEmojisRoute - AppOrgSlugSettingsDebugRoute: typeof AppOrgSlugSettingsDebugRoute - AppOrgSlugSettingsInvitationsRoute: typeof AppOrgSlugSettingsInvitationsRoute - AppOrgSlugSettingsTeamRoute: typeof AppOrgSlugSettingsTeamRoute - AppOrgSlugSettingsIndexRoute: typeof AppOrgSlugSettingsIndexRoute + AppOrgSlugSettingsChatSyncLayoutRoute: typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren + AppOrgSlugSettingsIntegrationsLayoutRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren + AppOrgSlugSettingsAuthenticationRoute: typeof AppOrgSlugSettingsAuthenticationRoute + AppOrgSlugSettingsConnectInvitesRoute: typeof AppOrgSlugSettingsConnectInvitesRoute + AppOrgSlugSettingsCustomEmojisRoute: typeof AppOrgSlugSettingsCustomEmojisRoute + AppOrgSlugSettingsDebugRoute: typeof AppOrgSlugSettingsDebugRoute + AppOrgSlugSettingsInvitationsRoute: typeof AppOrgSlugSettingsInvitationsRoute + AppOrgSlugSettingsTeamRoute: typeof AppOrgSlugSettingsTeamRoute + AppOrgSlugSettingsIndexRoute: typeof AppOrgSlugSettingsIndexRoute } -const AppOrgSlugSettingsLayoutRouteChildren: AppOrgSlugSettingsLayoutRouteChildren = { - AppOrgSlugSettingsChatSyncLayoutRoute: AppOrgSlugSettingsChatSyncLayoutRouteWithChildren, - AppOrgSlugSettingsIntegrationsLayoutRoute: AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren, - AppOrgSlugSettingsAuthenticationRoute: AppOrgSlugSettingsAuthenticationRoute, - AppOrgSlugSettingsConnectInvitesRoute: AppOrgSlugSettingsConnectInvitesRoute, - AppOrgSlugSettingsCustomEmojisRoute: AppOrgSlugSettingsCustomEmojisRoute, - AppOrgSlugSettingsDebugRoute: AppOrgSlugSettingsDebugRoute, - AppOrgSlugSettingsInvitationsRoute: AppOrgSlugSettingsInvitationsRoute, - AppOrgSlugSettingsTeamRoute: AppOrgSlugSettingsTeamRoute, - AppOrgSlugSettingsIndexRoute: AppOrgSlugSettingsIndexRoute, -} +const AppOrgSlugSettingsLayoutRouteChildren: AppOrgSlugSettingsLayoutRouteChildren = + { + AppOrgSlugSettingsChatSyncLayoutRoute: + AppOrgSlugSettingsChatSyncLayoutRouteWithChildren, + AppOrgSlugSettingsIntegrationsLayoutRoute: + AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren, + AppOrgSlugSettingsAuthenticationRoute: + AppOrgSlugSettingsAuthenticationRoute, + AppOrgSlugSettingsConnectInvitesRoute: + AppOrgSlugSettingsConnectInvitesRoute, + AppOrgSlugSettingsCustomEmojisRoute: AppOrgSlugSettingsCustomEmojisRoute, + AppOrgSlugSettingsDebugRoute: AppOrgSlugSettingsDebugRoute, + AppOrgSlugSettingsInvitationsRoute: AppOrgSlugSettingsInvitationsRoute, + AppOrgSlugSettingsTeamRoute: AppOrgSlugSettingsTeamRoute, + AppOrgSlugSettingsIndexRoute: AppOrgSlugSettingsIndexRoute, + } -const AppOrgSlugSettingsLayoutRouteWithChildren = AppOrgSlugSettingsLayoutRoute._addFileChildren( - AppOrgSlugSettingsLayoutRouteChildren, -) +const AppOrgSlugSettingsLayoutRouteWithChildren = + AppOrgSlugSettingsLayoutRoute._addFileChildren( + AppOrgSlugSettingsLayoutRouteChildren, + ) interface AppOrgSlugChatIdRouteChildren { - AppOrgSlugChatIdIndexRoute: typeof AppOrgSlugChatIdIndexRoute - AppOrgSlugChatIdFilesMediaRoute: typeof AppOrgSlugChatIdFilesMediaRoute - AppOrgSlugChatIdFilesIndexRoute: typeof AppOrgSlugChatIdFilesIndexRoute + AppOrgSlugChatIdIndexRoute: typeof AppOrgSlugChatIdIndexRoute + AppOrgSlugChatIdFilesMediaRoute: typeof AppOrgSlugChatIdFilesMediaRoute + AppOrgSlugChatIdFilesIndexRoute: typeof AppOrgSlugChatIdFilesIndexRoute } const AppOrgSlugChatIdRouteChildren: AppOrgSlugChatIdRouteChildren = { - AppOrgSlugChatIdIndexRoute: AppOrgSlugChatIdIndexRoute, - AppOrgSlugChatIdFilesMediaRoute: AppOrgSlugChatIdFilesMediaRoute, - AppOrgSlugChatIdFilesIndexRoute: AppOrgSlugChatIdFilesIndexRoute, + AppOrgSlugChatIdIndexRoute: AppOrgSlugChatIdIndexRoute, + AppOrgSlugChatIdFilesMediaRoute: AppOrgSlugChatIdFilesMediaRoute, + AppOrgSlugChatIdFilesIndexRoute: AppOrgSlugChatIdFilesIndexRoute, } -const AppOrgSlugChatIdRouteWithChildren = AppOrgSlugChatIdRoute._addFileChildren( - AppOrgSlugChatIdRouteChildren, -) +const AppOrgSlugChatIdRouteWithChildren = + AppOrgSlugChatIdRoute._addFileChildren(AppOrgSlugChatIdRouteChildren) interface AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren { - AppOrgSlugChannelsChannelIdSettingsConnectRoute: typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute - AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute: typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute - AppOrgSlugChannelsChannelIdSettingsOverviewRoute: typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute - AppOrgSlugChannelsChannelIdSettingsIndexRoute: typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute + AppOrgSlugChannelsChannelIdSettingsConnectRoute: typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute + AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute: typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute + AppOrgSlugChannelsChannelIdSettingsOverviewRoute: typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute + AppOrgSlugChannelsChannelIdSettingsIndexRoute: typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute } const AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren: AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren = - { - AppOrgSlugChannelsChannelIdSettingsConnectRoute: AppOrgSlugChannelsChannelIdSettingsConnectRoute, - AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute: - AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute, - AppOrgSlugChannelsChannelIdSettingsOverviewRoute: AppOrgSlugChannelsChannelIdSettingsOverviewRoute, - AppOrgSlugChannelsChannelIdSettingsIndexRoute: AppOrgSlugChannelsChannelIdSettingsIndexRoute, - } + { + AppOrgSlugChannelsChannelIdSettingsConnectRoute: + AppOrgSlugChannelsChannelIdSettingsConnectRoute, + AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute: + AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute, + AppOrgSlugChannelsChannelIdSettingsOverviewRoute: + AppOrgSlugChannelsChannelIdSettingsOverviewRoute, + AppOrgSlugChannelsChannelIdSettingsIndexRoute: + AppOrgSlugChannelsChannelIdSettingsIndexRoute, + } const AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren = - AppOrgSlugChannelsChannelIdSettingsLayoutRoute._addFileChildren( - AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren, - ) + AppOrgSlugChannelsChannelIdSettingsLayoutRoute._addFileChildren( + AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren, + ) interface AppOrgSlugLayoutRouteChildren { - AppOrgSlugMySettingsLayoutRoute: typeof AppOrgSlugMySettingsLayoutRouteWithChildren - AppOrgSlugNotificationsLayoutRoute: typeof AppOrgSlugNotificationsLayoutRouteWithChildren - AppOrgSlugSettingsLayoutRoute: typeof AppOrgSlugSettingsLayoutRouteWithChildren - AppOrgSlugIndexRoute: typeof AppOrgSlugIndexRoute - AppOrgSlugChatIdRoute: typeof AppOrgSlugChatIdRouteWithChildren - AppOrgSlugProfileUserIdRoute: typeof AppOrgSlugProfileUserIdRoute - AppOrgSlugChatIndexRoute: typeof AppOrgSlugChatIndexRoute - AppOrgSlugChannelsChannelIdSettingsLayoutRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren + AppOrgSlugMySettingsLayoutRoute: typeof AppOrgSlugMySettingsLayoutRouteWithChildren + AppOrgSlugNotificationsLayoutRoute: typeof AppOrgSlugNotificationsLayoutRouteWithChildren + AppOrgSlugSettingsLayoutRoute: typeof AppOrgSlugSettingsLayoutRouteWithChildren + AppOrgSlugIndexRoute: typeof AppOrgSlugIndexRoute + AppOrgSlugChatIdRoute: typeof AppOrgSlugChatIdRouteWithChildren + AppOrgSlugProfileUserIdRoute: typeof AppOrgSlugProfileUserIdRoute + AppOrgSlugChatIndexRoute: typeof AppOrgSlugChatIndexRoute + AppOrgSlugChannelsChannelIdSettingsLayoutRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren } const AppOrgSlugLayoutRouteChildren: AppOrgSlugLayoutRouteChildren = { - AppOrgSlugMySettingsLayoutRoute: AppOrgSlugMySettingsLayoutRouteWithChildren, - AppOrgSlugNotificationsLayoutRoute: AppOrgSlugNotificationsLayoutRouteWithChildren, - AppOrgSlugSettingsLayoutRoute: AppOrgSlugSettingsLayoutRouteWithChildren, - AppOrgSlugIndexRoute: AppOrgSlugIndexRoute, - AppOrgSlugChatIdRoute: AppOrgSlugChatIdRouteWithChildren, - AppOrgSlugProfileUserIdRoute: AppOrgSlugProfileUserIdRoute, - AppOrgSlugChatIndexRoute: AppOrgSlugChatIndexRoute, - AppOrgSlugChannelsChannelIdSettingsLayoutRoute: - AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren, + AppOrgSlugMySettingsLayoutRoute: AppOrgSlugMySettingsLayoutRouteWithChildren, + AppOrgSlugNotificationsLayoutRoute: + AppOrgSlugNotificationsLayoutRouteWithChildren, + AppOrgSlugSettingsLayoutRoute: AppOrgSlugSettingsLayoutRouteWithChildren, + AppOrgSlugIndexRoute: AppOrgSlugIndexRoute, + AppOrgSlugChatIdRoute: AppOrgSlugChatIdRouteWithChildren, + AppOrgSlugProfileUserIdRoute: AppOrgSlugProfileUserIdRoute, + AppOrgSlugChatIndexRoute: AppOrgSlugChatIndexRoute, + AppOrgSlugChannelsChannelIdSettingsLayoutRoute: + AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren, } -const AppOrgSlugLayoutRouteWithChildren = AppOrgSlugLayoutRoute._addFileChildren( - AppOrgSlugLayoutRouteChildren, -) +const AppOrgSlugLayoutRouteWithChildren = + AppOrgSlugLayoutRoute._addFileChildren(AppOrgSlugLayoutRouteChildren) interface AppLayoutRouteChildren { - AppOrgSlugLayoutRoute: typeof AppOrgSlugLayoutRouteWithChildren - AppIndexRoute: typeof AppIndexRoute - AppOnboardingSetupOrganizationRoute: typeof AppOnboardingSetupOrganizationRoute - AppOnboardingIndexRoute: typeof AppOnboardingIndexRoute - AppSelectOrganizationIndexRoute: typeof AppSelectOrganizationIndexRoute + AppOrgSlugLayoutRoute: typeof AppOrgSlugLayoutRouteWithChildren + AppIndexRoute: typeof AppIndexRoute + AppOnboardingSetupOrganizationRoute: typeof AppOnboardingSetupOrganizationRoute + AppOnboardingIndexRoute: typeof AppOnboardingIndexRoute + AppSelectOrganizationIndexRoute: typeof AppSelectOrganizationIndexRoute } const AppLayoutRouteChildren: AppLayoutRouteChildren = { - AppOrgSlugLayoutRoute: AppOrgSlugLayoutRouteWithChildren, - AppIndexRoute: AppIndexRoute, - AppOnboardingSetupOrganizationRoute: AppOnboardingSetupOrganizationRoute, - AppOnboardingIndexRoute: AppOnboardingIndexRoute, - AppSelectOrganizationIndexRoute: AppSelectOrganizationIndexRoute, + AppOrgSlugLayoutRoute: AppOrgSlugLayoutRouteWithChildren, + AppIndexRoute: AppIndexRoute, + AppOnboardingSetupOrganizationRoute: AppOnboardingSetupOrganizationRoute, + AppOnboardingIndexRoute: AppOnboardingIndexRoute, + AppSelectOrganizationIndexRoute: AppSelectOrganizationIndexRoute, } -const AppLayoutRouteWithChildren = AppLayoutRoute._addFileChildren(AppLayoutRouteChildren) +const AppLayoutRouteWithChildren = AppLayoutRoute._addFileChildren( + AppLayoutRouteChildren, +) interface DevUiLayoutRouteChildren { - DevUiAgentStepsRoute: typeof DevUiAgentStepsRoute + DevUiAgentStepsRoute: typeof DevUiAgentStepsRoute } const DevUiLayoutRouteChildren: DevUiLayoutRouteChildren = { - DevUiAgentStepsRoute: DevUiAgentStepsRoute, + DevUiAgentStepsRoute: DevUiAgentStepsRoute, } -const DevUiLayoutRouteWithChildren = DevUiLayoutRoute._addFileChildren(DevUiLayoutRouteChildren) +const DevUiLayoutRouteWithChildren = DevUiLayoutRoute._addFileChildren( + DevUiLayoutRouteChildren, +) interface DevLayoutRouteChildren { - DevUiLayoutRoute: typeof DevUiLayoutRouteWithChildren + DevUiLayoutRoute: typeof DevUiLayoutRouteWithChildren } const DevLayoutRouteChildren: DevLayoutRouteChildren = { - DevUiLayoutRoute: DevUiLayoutRouteWithChildren, + DevUiLayoutRoute: DevUiLayoutRouteWithChildren, } -const DevLayoutRouteWithChildren = DevLayoutRoute._addFileChildren(DevLayoutRouteChildren) +const DevLayoutRouteWithChildren = DevLayoutRoute._addFileChildren( + DevLayoutRouteChildren, +) const rootRouteChildren: RootRouteChildren = { - AppLayoutRoute: AppLayoutRouteWithChildren, - DevLayoutRoute: DevLayoutRouteWithChildren, - AuthCallbackRoute: AuthCallbackRoute, - AuthDesktopCallbackRoute: AuthDesktopCallbackRoute, - AuthDesktopLoginRoute: AuthDesktopLoginRoute, - AuthLoginRoute: AuthLoginRoute, - JoinSlugRoute: JoinSlugRoute, - DevEmbedsDemoRoute: DevEmbedsDemoRoute, - DevEmbedsGithubRoute: DevEmbedsGithubRoute, - DevEmbedsOpenstatusRoute: DevEmbedsOpenstatusRoute, - DevEmbedsRailwayRoute: DevEmbedsRailwayRoute, - DevEmbedsIndexRoute: DevEmbedsIndexRoute, + AppLayoutRoute: AppLayoutRouteWithChildren, + DevLayoutRoute: DevLayoutRouteWithChildren, + AuthCallbackRoute: AuthCallbackRoute, + AuthDesktopCallbackRoute: AuthDesktopCallbackRoute, + AuthDesktopLoginRoute: AuthDesktopLoginRoute, + AuthLoginRoute: AuthLoginRoute, + JoinSlugRoute: JoinSlugRoute, + DevEmbedsDemoRoute: DevEmbedsDemoRoute, + DevEmbedsGithubRoute: DevEmbedsGithubRoute, + DevEmbedsOpenstatusRoute: DevEmbedsOpenstatusRoute, + DevEmbedsRailwayRoute: DevEmbedsRailwayRoute, + DevEmbedsIndexRoute: DevEmbedsIndexRoute, } -export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes() +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 66224f265..3dc71e10d 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -1,4 +1,3 @@ -import { TanStackDevtools } from "@tanstack/react-devtools" import { createRootRouteWithContext, type NavigateOptions, @@ -6,7 +5,6 @@ import { type ToOptions, useRouter, } from "@tanstack/react-router" -import { RpcDevtoolsPanel } from "effect-rpc-tanstack-devtools/components" import { lazy, Suspense } from "react" import { RouterProvider } from "react-aria-components" diff --git a/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/connect.tsx b/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/connect.tsx index ff0016fd9..c69011474 100644 --- a/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/connect.tsx +++ b/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/connect.tsx @@ -2,7 +2,7 @@ import { AsyncResult } from "effect/unstable/reactivity" import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { ChannelId, ConnectConversationId, ConnectInviteId, OrganizationId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" -import { Option } from "effect" +import { type DateTime, Option } from "effect" import { createFileRoute } from "@tanstack/react-router" import { useMemo, useState } from "react" import { @@ -28,6 +28,7 @@ import { getConnectInviteStatusBadge, getSharedConversationMountsForChannel, } from "~/lib/connect-shared-channels" +import { toDate } from "~/lib/utils" import { exitToastAsync } from "~/lib/toast-exit" export const Route = createFileRoute("/_app/$orgSlug/channels/$channelId/settings/connect")({ @@ -68,7 +69,11 @@ function ConnectPage() { return data.value.data.filter((inv) => inv.hostChannelId === channelId) }, [outgoingResult, channelId]) - const sharedConnections = getSharedConversationMountsForChannel(channelId as ChannelId, connections ?? []) + type SharedConnection = NonNullable[number] + const sharedConnections: SharedConnection[] = getSharedConversationMountsForChannel( + channelId as ChannelId, + connections ?? [], + ) const isConnected = sharedConnections.length > 0 const viewerMount = sharedConnections.find((connection) => connection.organizationId === organizationId) @@ -279,7 +284,7 @@ function InviteRow({ targetKind: string targetValue: string status: string - createdAt: Date + createdAt: Date | DateTime.Utc } organizationId: OrganizationId | undefined }) { @@ -329,7 +334,7 @@ function InviteRow({ - {invite.createdAt.toLocaleDateString()} + {toDate(invite.createdAt).toLocaleDateString()} {invite.status === "pending" && ( diff --git a/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/integrations.tsx b/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/integrations.tsx index 8c0b92292..d8827c380 100644 --- a/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/integrations.tsx +++ b/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/integrations.tsx @@ -4,6 +4,7 @@ import { eq, useLiveQuery } from "@tanstack/react-db" import { createFileRoute } from "@tanstack/react-router" import { formatDistanceToNow } from "date-fns" import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { toDate } from "~/lib/utils" import { toast } from "sonner" import { deleteChannelWebhookMutation, @@ -279,7 +280,7 @@ function CompactWebhookItem({ webhook, onDelete }: { webhook: WebhookData; onDel <> · - {formatDistanceToNow(new Date(webhook.lastUsedAt), { addSuffix: true })} + {formatDistanceToNow(toDate(webhook.lastUsedAt), { addSuffix: true })} )} diff --git a/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/$connectionId.tsx b/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/$connectionId.tsx index 15c0b1e5e..d407d1e24 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/$connectionId.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/$connectionId.tsx @@ -5,6 +5,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router" import { eq, useLiveQuery } from "@tanstack/react-db" import { Option } from "effect" import { useMemo, useState } from "react" +import { toDate } from "~/lib/utils" import { AddChannelLinkModal } from "~/components/chat-sync/add-channel-link-modal" import IconCirclePause from "~/components/icons/icon-circle-pause" import IconDotsVertical from "~/components/icons/icon-dots-vertical" @@ -454,7 +455,7 @@ function ChatSyncConnectionDetailPage() {

{connection.lastSyncedAt - ? `Last synced ${new Date( + ? `Last synced ${toDate( connection.lastSyncedAt, ).toLocaleDateString(undefined, { month: "short", diff --git a/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/index.tsx b/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/index.tsx index cedf140b2..a62946505 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/index.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/chat-sync/index.tsx @@ -3,6 +3,7 @@ import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { SyncConnectionId } from "@hazel/schema" import { createFileRoute, useNavigate } from "@tanstack/react-router" import { Option } from "effect" +import { toDate } from "~/lib/utils" import { useState } from "react" import { AddConnectionModal } from "~/components/chat-sync/add-connection-modal" import IconArrowPath from "~/components/icons/icon-arrow-path" @@ -264,7 +265,7 @@ function ChatSyncConnectionsPage() { {connection.lastSyncedAt && ( Last synced:{" "} - {new Date(connection.lastSyncedAt).toLocaleDateString( + {toDate(connection.lastSyncedAt).toLocaleDateString( undefined, { month: "short", diff --git a/apps/web/src/routes/_app/$orgSlug/settings/connect-invites.tsx b/apps/web/src/routes/_app/$orgSlug/settings/connect-invites.tsx index d916017bb..d80df1026 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/connect-invites.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/connect-invites.tsx @@ -2,7 +2,7 @@ import { AsyncResult } from "effect/unstable/reactivity" import { useAtomSet, useAtomValue } from "@effect/atom-react" import type { ConnectInviteId, OrganizationId } from "@hazel/schema" import { eq, useLiveQuery } from "@tanstack/react-db" -import { Option } from "effect" +import { type DateTime, Option } from "effect" import { createFileRoute } from "@tanstack/react-router" import { useMemo, useState } from "react" import { @@ -18,6 +18,7 @@ import { EmptyState } from "~/components/ui/empty-state" import { organizationCollection } from "~/db/collections" import { useOrganization } from "~/hooks/use-organization" import { getConnectInviteStatusBadge } from "~/lib/connect-shared-channels" +import { toDate } from "~/lib/utils" import { exitToastAsync } from "~/lib/toast-exit" export const Route = createFileRoute("/_app/$orgSlug/settings/connect-invites")({ @@ -115,7 +116,7 @@ function IncomingInviteRow({ id: ConnectInviteId hostOrganizationId: OrganizationId status: string - createdAt: Date + createdAt: Date | DateTime.Utc } organizationId: OrganizationId | undefined }) { @@ -228,7 +229,7 @@ function IncomingInviteRow({ - {invite.createdAt.toLocaleDateString()} + {toDate(invite.createdAt).toLocaleDateString()} {invite.status === "pending" && ( diff --git a/apps/web/src/routes/_app/$orgSlug/settings/custom-emojis.tsx b/apps/web/src/routes/_app/$orgSlug/settings/custom-emojis.tsx index 1fac39189..fd9218592 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/custom-emojis.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/custom-emojis.tsx @@ -1,8 +1,10 @@ import { useAtomSet } from "@effect/atom-react" +import { CustomEmojiDeletedExistsError } from "@hazel/domain/rpc" import { eq, isNull, useLiveQuery } from "@tanstack/react-db" import { createFileRoute } from "@tanstack/react-router" import { formatDistanceToNow } from "date-fns" -import { Cause, Chunk, Exit, Option } from "effect" +import { toDate } from "~/lib/utils" +import { Cause, Exit } from "effect" import { useCallback, useEffect, useState } from "react" import { type DropItem, DropZone, FileTrigger, Button as AriaButton } from "react-aria-components" import { toast } from "sonner" @@ -204,27 +206,19 @@ function CustomEmojisSettings() { toast.success(`Emoji :${emojiName}: created`) handleCancel() } else { - // Check if the error is a deleted emoji conflict - const failures = Cause.failures(createResult.cause) - const firstError = Chunk.head(failures) - - if (Option.isSome(firstError)) { - if ("_tag" in firstError.value) { - if (firstError.value._tag === "CustomEmojiDeletedExistsError") { - const err = firstError.value as { - customEmojiId: CustomEmojiId - name: string - imageUrl: string - } - setRestoreTarget({ - id: err.customEmojiId, - name: err.name, - imageUrl: err.imageUrl, - newImageUrl: publicUrl, - }) - return - } - } + const deletedExistsError = createResult.cause.reasons + .filter(Cause.isFailReason) + .map((reason) => reason.error) + .find((error): error is CustomEmojiDeletedExistsError => error instanceof CustomEmojiDeletedExistsError) + + if (deletedExistsError) { + setRestoreTarget({ + id: deletedExistsError.customEmojiId, + name: deletedExistsError.name, + imageUrl: deletedExistsError.imageUrl, + newImageUrl: publicUrl, + }) + return } toast.error("Failed to create emoji", { description: "The name may already be taken. Please try another.", @@ -528,7 +522,7 @@ function CustomEmojisSettings() { {emoji.createdAt - ? formatDistanceToNow(new Date(emoji.createdAt), { + ? formatDistanceToNow(toDate(emoji.createdAt), { addSuffix: true, }) : "—"} diff --git a/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx b/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx index d02cd76a6..d727e72f6 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx @@ -186,9 +186,9 @@ function IntegrationConfigPage() { description: "This integration is already disconnected.", isRetryable: false, })) - .onErrorTag("UnsupportedProviderError", (error) => ({ + .onErrorTag("UnsupportedProviderError", () => ({ title: "Unsupported provider", - description: `The provider "${error.provider}" is not supported.`, + description: "This integration provider is not supported.", isRetryable: false, })) .run() @@ -216,9 +216,9 @@ function IntegrationConfigPage() { description: error.message, isRetryable: true, })) - .onErrorTag("UnsupportedProviderError", (error) => ({ + .onErrorTag("UnsupportedProviderError", () => ({ title: "Unsupported provider", - description: `The provider "${error.provider}" does not support API key connections.`, + description: "This integration does not support API key connections.", isRetryable: false, })) .onError(() => ({ diff --git a/apps/web/src/routes/_app/onboarding/setup-organization.tsx b/apps/web/src/routes/_app/onboarding/setup-organization.tsx index 73db80212..b912adc0e 100644 --- a/apps/web/src/routes/_app/onboarding/setup-organization.tsx +++ b/apps/web/src/routes/_app/onboarding/setup-organization.tsx @@ -93,9 +93,9 @@ function RouteComponent() { }) exitToast(exit) - .onErrorTag("OrganizationSlugAlreadyExistsError", (error) => ({ + .onErrorTag("OrganizationSlugAlreadyExistsError", () => ({ title: "Slug already taken", - description: `The slug "${error.slug}" is already in use. Please choose a different one.`, + description: "That workspace URL is already in use. Please choose a different one.", isRetryable: false, })) .onErrorTag("OrganizationNotFoundError", () => ({ diff --git a/apps/web/src/routes/auth/callback.tsx b/apps/web/src/routes/auth/callback.tsx index f16008398..d75ab0b21 100644 --- a/apps/web/src/routes/auth/callback.tsx +++ b/apps/web/src/routes/auth/callback.tsx @@ -25,7 +25,7 @@ const AuthStateSchema = Schema.Struct({ // Schema for search params - state can be string or parsed object (TanStack Router auto-parses JSON) const RawSearchParams = Schema.Struct({ code: Schema.optional(Schema.String), - state: Schema.optional(Schema.Union(Schema.String, AuthStateSchema)), + state: Schema.optional(Schema.Union([Schema.String, AuthStateSchema])), error: Schema.optional(Schema.String), error_description: Schema.optional(Schema.String), }) diff --git a/libs/bot-sdk/src/gateway.test.ts b/libs/bot-sdk/src/gateway.test.ts index 3c2d9a82d..2c561cf01 100644 --- a/libs/bot-sdk/src/gateway.test.ts +++ b/libs/bot-sdk/src/gateway.test.ts @@ -9,7 +9,7 @@ import { createGatewayWebSocketUrl, } from "./gateway.ts" -const BOT_ID = "00000000-0000-0000-0000-000000000111" as BotId +const BOT_ID = "00000000-0000-4000-8000-000000000111" as BotId describe("InMemoryGatewaySessionStoreLive", () => { it("loads and saves offsets per bot", () => diff --git a/libs/bot-sdk/src/hazel-bot-sdk.error-handling.test.ts b/libs/bot-sdk/src/hazel-bot-sdk.error-handling.test.ts index 53883405d..f14065c65 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.error-handling.test.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.error-handling.test.ts @@ -11,12 +11,16 @@ import { BotStateStoreTag, GatewaySessionStoreTag } from "./gateway.ts" import { HazelBotClient, HazelBotRuntimeConfigTag } from "./hazel-bot-sdk.ts" import { BotRpcClient, BotRpcClientConfigTag } from "./rpc/client.ts" +vi.mock("@hazel/effect-bun/Telemetry", () => ({ + createTracingLayer: () => Layer.empty, +})) + const BACKEND_URL = "http://localhost:3070" const GATEWAY_URL = "http://localhost:3034" -const BOT_ID = "00000000-0000-0000-0000-000000000111" -const USER_ID = "00000000-0000-0000-0000-000000000222" -const ORG_ID = "00000000-0000-0000-0000-000000000333" -const CHANNEL_ID = "00000000-0000-0000-0000-000000000444" +const BOT_ID = "00000000-0000-4000-8000-000000000111" +const USER_ID = "00000000-0000-4000-8000-000000000222" +const ORG_ID = "00000000-0000-4000-8000-000000000333" +const CHANNEL_ID = "00000000-0000-4000-8000-000000000444" const BOT_TOKEN = "test-bot-token" const EchoCommand = Command.make("echo", { @@ -37,7 +41,7 @@ const server = setupServer() const makeMessageResponse = (content: string) => ({ data: { - id: "00000000-0000-0000-0000-000000000999", + id: "00000000-0000-4000-8000-000000000999", channelId: CHANNEL_ID, authorId: USER_ID, content, @@ -48,7 +52,7 @@ const makeMessageResponse = (content: string) => ({ updatedAt: null, deletedAt: null, }, - transactionId: "00000000-0000-0000-0000-000000000998", + transactionId: "00000000-0000-4000-8000-000000000998", }) const makeHazelBotLayer = () => diff --git a/libs/bot-sdk/src/hazel-bot-sdk.test.ts b/libs/bot-sdk/src/hazel-bot-sdk.test.ts index c815c8485..69f21bf27 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.test.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.test.ts @@ -2,6 +2,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "@effect/vi import { Duration, Effect, Layer, Ref } from "effect" import { delay, http, HttpResponse } from "msw" import { setupServer } from "msw/node" +import { vi } from "vitest" import { BotGatewayAckFrame, BotGatewayClientFrame, @@ -17,12 +18,16 @@ import { BotRpcClient, BotRpcClientConfigTag } from "./rpc/client.ts" import { BotStateStoreTag, GatewaySessionStoreTag } from "./gateway.ts" import { Schema } from "effect" +vi.mock("@hazel/effect-bun/Telemetry", () => ({ + createTracingLayer: () => Layer.empty, +})) + const BACKEND_URL = "http://localhost:3070" const GATEWAY_URL = "http://localhost:3034" -const BOT_ID = "00000000-0000-0000-0000-000000000111" -const USER_ID = "00000000-0000-0000-0000-000000000222" -const ORG_ID = "00000000-0000-0000-0000-000000000333" -const CHANNEL_ID = "00000000-0000-0000-0000-000000000444" +const BOT_ID = "00000000-0000-4000-8000-000000000111" +const USER_ID = "00000000-0000-4000-8000-000000000222" +const ORG_ID = "00000000-0000-4000-8000-000000000333" +const CHANNEL_ID = "00000000-0000-4000-8000-000000000444" const BOT_TOKEN = "test-bot-token" const EchoCommand = Command.make("echo", { @@ -316,7 +321,7 @@ describe("HazelBotClient durable gateway", () => { const bot = yield* HazelBotClient yield* bot.onCommand(EchoCommand, () => Ref.update(attemptsRef, (attempts) => attempts + 1).pipe( - Effect.zipRight(Effect.fail(new Error("boom"))), + Effect.andThen(Effect.fail(new Error("boom"))), ), ) yield* bot.start diff --git a/libs/bot-sdk/src/hazel-bot-sdk.ts b/libs/bot-sdk/src/hazel-bot-sdk.ts index 7fd6b4895..ca9349e3c 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.ts @@ -33,7 +33,14 @@ import { isTemporaryActorServiceError, } from "@hazel/domain" import type { IntegrationConnection } from "@hazel/domain/models" -import { HazelApi } from "@hazel/domain/http" +import { + CreateMessageRequest, + HazelApi, + SyncBotCommandsRequest, + ToggleReactionRequest, + UpdateBotSettingsRequest, + UpdateMessageRequest, +} from "@hazel/domain/http" import { Channel, ChannelMember, Message } from "@hazel/domain/models" import { createTracingLayer } from "@hazel/effect-bun/Telemetry" import { @@ -95,6 +102,7 @@ import { type AIStreamOptions, type AIStreamSession, type CreateStreamOptions, + type MessageCreateFn, type MessageUpdateFn, } from "./streaming/index.ts" @@ -876,14 +884,14 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB // Call the sync endpoint using type-safe HttpApiClient const response = yield* httpApiClient["bot-commands"].syncCommands({ - payload: { + payload: new SyncBotCommandsRequest({ commands: cmds.map((cmd: CommandDef) => ({ name: cmd.name, description: cmd.description, arguments: schemaFieldsToArgs(cmd.args), usageExample: cmd.usageExample ?? null, })), - }, + }), }) yield* Effect.logDebug(`Synced ${response.syncedCount} commands successfully`) @@ -909,7 +917,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB yield* Effect.logDebug(`Syncing mentionable=${config.mentionable} with backend...`) yield* httpApiClient["bot-commands"].updateBotSettings({ - payload: { mentionable: config.mentionable }, + payload: new UpdateBotSettingsRequest({ mentionable: config.mentionable }), }) yield* Effect.logDebug("Mentionable flag synced successfully") @@ -955,7 +963,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB * Helper to create the message creation function. * Shared between stream.create and ai.stream to avoid code duplication. */ - const createMessageFnHelper = ( + const createMessageFnHelper: MessageCreateFn = ( chId: ChannelId, content: string, opts?: { @@ -979,13 +987,13 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB messageLimiter( httpApiClient["api-v1-messages"] .createMessage({ - payload: { + payload: new CreateMessageRequest({ channelId: chId, content, replyToMessageId: opts?.replyToMessageId ?? null, threadChannelId: opts?.threadChannelId ?? null, embeds: opts?.embeds ?? null, - }, + }), }) .pipe( Effect.map((r) => r.data), @@ -1030,10 +1038,10 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB httpApiClient["api-v1-messages"] .updateMessage({ params: { id: messageId }, - payload: { + payload: new UpdateMessageRequest({ content: payload.content, embeds: payload.embeds ?? null, - }, + }), }) .pipe( Effect.map((r) => r.data), @@ -1046,7 +1054,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB }), ), ), - ) as any + ) return { /** @@ -1150,7 +1158,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB messageLimiter( httpApiClient["api-v1-messages"] .createMessage({ - payload: { + payload: new CreateMessageRequest({ channelId, content, replyToMessageId: options?.replyToMessageId ?? null, @@ -1159,7 +1167,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB ? [...options.attachmentIds] : undefined, embeds: options?.embeds ?? null, - }, + }), }) .pipe( Effect.map((r) => r.data), @@ -1189,7 +1197,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB messageLimiter( httpApiClient["api-v1-messages"] .createMessage({ - payload: { + payload: new CreateMessageRequest({ channelId: message.channelId, content, replyToMessageId: message.id, @@ -1198,7 +1206,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB ? [...options.attachmentIds] : undefined, embeds: null, - }, + }), }) .pipe( Effect.map((r) => r.data), @@ -1230,7 +1238,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB httpApiClient["api-v1-messages"] .updateMessage({ params: { id: message.id }, - payload: { content }, + payload: new UpdateMessageRequest({ content }), }) .pipe( Effect.map((r) => r.data), @@ -1281,10 +1289,10 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB httpApiClient["api-v1-messages"] .toggleReaction({ params: { id: message.id }, - payload: { + payload: new ToggleReactionRequest({ emoji, channelId: message.channelId, - }, + }), }) .pipe( Effect.mapError( @@ -1648,7 +1656,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB const actorsService = yield* createActorsServiceFn() return yield* createStreamSessionInternal( - createMessageFnHelper as any, + createMessageFnHelper, updateMessageFnHelper, actorsService, channelId, @@ -1683,7 +1691,7 @@ export class HazelBotClient extends ServiceMap.Service()("HazelB const actorsService = yield* createActorsServiceFn() return yield* createAIStreamSessionInternal( - createMessageFnHelper as any, + createMessageFnHelper, updateMessageFnHelper, actorsService, channelId, diff --git a/libs/bot-sdk/src/run-bot.ts b/libs/bot-sdk/src/run-bot.ts index 1144db188..425769769 100644 --- a/libs/bot-sdk/src/run-bot.ts +++ b/libs/bot-sdk/src/run-bot.ts @@ -9,7 +9,7 @@ */ import { BunRuntime } from "@effect/platform-bun" -import { Effect, Layer, Redacted } from "effect" +import { Effect, Layer, Redacted, ServiceMap } from "effect" import type { CommandGroup, EmptyCommands } from "./command.ts" import { createHazelBot, HazelBotClient, type HazelBotConfig } from "./hazel-bot-sdk.ts" import { BotEnvConfig } from "./bot-config.ts" @@ -47,7 +47,9 @@ export interface RunBotConfig = EmptyCommands * Receives the HazelBotClient and should register handlers. * Does NOT need to call bot.start - that's handled automatically. */ - readonly setup: (bot: HazelBotClient) => Effect.Effect + readonly setup: ( + bot: ServiceMap.Service.Shape, + ) => Effect.Effect /** * Optional override for bot configuration. @@ -131,7 +133,7 @@ export const runHazelBot = = EmptyCommands>( const bot = yield* HazelBotClient // Run user's setup function - yield* options.setup(bot as any) + yield* options.setup(bot) // Start the bot yield* bot.start diff --git a/libs/bot-sdk/src/streaming/streaming-service.ts b/libs/bot-sdk/src/streaming/streaming-service.ts index 8e45f9caf..8a686282b 100644 --- a/libs/bot-sdk/src/streaming/streaming-service.ts +++ b/libs/bot-sdk/src/streaming/streaming-service.ts @@ -51,7 +51,7 @@ export type MessageCreateFn = ( }[] | null }, -) => Effect.Effect<{ id: string }, unknown> +) => Effect.Effect<{ id: string }, unknown, unknown> /** * Message update function type - matches the message update API @@ -73,7 +73,7 @@ export type MessageUpdateFn = ( } }> | null }, -) => Effect.Effect<{ id: string }, unknown> +) => Effect.Effect<{ id: string }, unknown, unknown> /** * Wrap an actor method call with Effect, error handling, and tracing. diff --git a/libs/bot-sdk/src/streaming/types.ts b/libs/bot-sdk/src/streaming/types.ts index 932f94857..24ac5ea37 100644 --- a/libs/bot-sdk/src/streaming/types.ts +++ b/libs/bot-sdk/src/streaming/types.ts @@ -65,10 +65,10 @@ export interface StreamSession { ): Effect.Effect /** Mark the stream as completed */ - complete(finalData?: Record): Effect.Effect + complete(finalData?: Record): Effect.Effect /** Mark the stream as failed */ - fail(error: string): Effect.Effect + fail(error: string): Effect.Effect } /** diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.result.test.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.result.test.ts index 10d33ac7f..403b663c6 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.result.test.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.result.test.ts @@ -9,7 +9,11 @@ * @since 1.0.0 */ -import { Atom, Registry, Result } from "@effect/atom-react" +import { + Atom, + AsyncResult as Result, + AtomRegistry as Registry, +} from "effect/unstable/reactivity" import { type Collection, createCollection, eq, type NonSingleResult } from "@tanstack/db" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { makeCollectionAtom, makeQuery, makeQueryConditional, makeQueryUnsafe } from "./AtomTanStackDB" diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.subscription.test.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.subscription.test.ts index 1343b30ba..b6472af8a 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.subscription.test.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.subscription.test.ts @@ -9,7 +9,11 @@ * @since 1.0.0 */ -import { Atom, Registry, Result } from "@effect/atom-react" +import { + Atom, + AsyncResult as Result, + AtomRegistry as Registry, +} from "effect/unstable/reactivity" import { type Collection, createCollection, eq, type NonSingleResult } from "@tanstack/db" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { makeCollectionAtom, makeQuery } from "./AtomTanStackDB" diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.test.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.test.ts index e7f143d1d..1552c9e30 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.test.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.test.ts @@ -16,7 +16,7 @@ * @since 1.0.0 */ -import { Registry, Result } from "@effect/atom-react" +import { AsyncResult as Result, AtomRegistry as Registry } from "effect/unstable/reactivity" import { type Collection, createCollection, eq, type NonSingleResult, type SingleResult } from "@tanstack/db" import { describe, expect, it } from "vitest" import { @@ -205,7 +205,7 @@ describe("makeCollectionAtom", () => { const todosAtom = makeCollectionAtom(collection) // Track updates - const updates: Array, Error>> = [] + const updates: Array, Error>> = [] const unsubscribe = registry.subscribe(todosAtom, (value) => { updates.push(value) }) diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.timing.test.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.timing.test.ts index 432bad1b7..5198f2867 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.timing.test.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.timing.test.ts @@ -9,7 +9,11 @@ * @since 1.0.0 */ -import { Atom, Registry, Result } from "@effect/atom-react" +import { + Atom, + AsyncResult as Result, + AtomRegistry as Registry, +} from "effect/unstable/reactivity" import { type Collection, createCollection, eq, type NonSingleResult } from "@tanstack/db" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { makeCollectionAtom, makeQuery } from "./AtomTanStackDB" @@ -230,7 +234,7 @@ describe("Timing and Async Behavior", () => { const todosAtom = makeCollectionAtom(collection) - const updates: Array, Error>> = [] + const updates: Array, Error>> = [] registry.subscribe(todosAtom, (value) => { updates.push(value) }) diff --git a/packages/actors/src/actors/message-actor.create-conn-state.test.ts b/packages/actors/src/actors/message-actor.create-conn-state.test.ts index 41443c9d2..3ed3e20f0 100644 --- a/packages/actors/src/actors/message-actor.create-conn-state.test.ts +++ b/packages/actors/src/actors/message-actor.create-conn-state.test.ts @@ -1,6 +1,18 @@ import { createServer } from "node:http" -import { exportJWK, generateKeyPair, SignJWT } from "jose" +import { BotId, OrganizationId, UserId, WorkOSOrganizationId, WorkOSUserId } from "@hazel/schema" +import { decodeJwt, exportJWK, generateKeyPair, SignJWT } from "jose" import { afterEach, describe, expect, it, vi } from "vitest" +import type * as EffectType from "effect/Effect" +import type * as HttpClientType from "effect/unstable/http/HttpClient" +import type { + AuthenticatedClient, + BotClient, + BotTokenValidationError, + ConfigError, + InvalidTokenFormatError, + JwtValidationError, + UserClient, +} from "../auth" const ORIGINAL_ENV = { ...process.env } @@ -31,11 +43,170 @@ const createContext = () => ({ const loadCreateConnState = async () => { vi.resetModules() + vi.doMock("../effect/runtime", async () => { + const { Effect, Layer, ManagedRuntime, Schema } = await import("effect") + const { FetchHttpClient, HttpClient } = await import("effect/unstable/http") + const auth = await import("../auth") + const { + BotTokenValidationError, + ConfigError, + InvalidTokenFormatError, + JwtValidationError, + TokenValidationService, + } = auth + + const decodeUserId = Schema.decodeUnknownSync(UserId) + const decodeBotId = Schema.decodeUnknownSync(BotId) + const decodeOrganizationId = Schema.decodeUnknownSync(OrganizationId) + const decodeWorkOsUserId = Schema.decodeUnknownSync(WorkOSUserId) + const decodeWorkOsOrganizationId = Schema.decodeUnknownSync(WorkOSOrganizationId) + + const validateBotToken = ( + token: string, + ): EffectType.Effect< + BotClient, + BotTokenValidationError | ConfigError, + HttpClientType.HttpClient + > => + Effect.gen(function* () { + yield* HttpClient.HttpClient + + const backendUrl = + process.env.BACKEND_URL ?? + process.env.API_BASE_URL ?? + process.env.VITE_BACKEND_URL ?? + process.env.VITE_API_BASE_URL + + if (!backendUrl) { + return yield* Effect.fail( + new ConfigError({ + message: + "BACKEND_URL or API_BASE_URL environment variable is required for bot token actor authentication", + }), + ) + } + + const response = yield* Effect.tryPromise({ + try: () => + fetch(`${backendUrl}/internal/actors/validate-bot-token`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token }), + }), + catch: (cause) => + new BotTokenValidationError({ + message: `Failed to validate bot token: ${String(cause)}`, + }), + }) + + if (!response.ok) { + const errorText = yield* Effect.promise(() => response.text()) + return yield* Effect.fail( + new BotTokenValidationError({ + message: `Invalid bot token: ${errorText}`, + statusCode: response.status, + }), + ) + } + + const data = (yield* Effect.promise(() => response.json())) as { + userId: string + botId: string + organizationId: string | null + scopes: readonly string[] | null + } + + return { + type: "bot" as const, + userId: decodeUserId(data.userId), + botId: decodeBotId(data.botId), + organizationId: + data.organizationId === null ? null : decodeOrganizationId(data.organizationId), + scopes: data.scopes, + } + }) + + const validateJwt = ( + token: string, + ): EffectType.Effect => + Effect.gen(function* () { + if (!process.env.WORKOS_CLIENT_ID) { + return yield* Effect.fail( + new ConfigError({ + message: + "WORKOS_CLIENT_ID environment variable is required for JWT actor authentication", + }), + ) + } + + const claims = yield* Effect.try({ + try: () => decodeJwt(token), + catch: (cause) => + new JwtValidationError({ + message: "Invalid or expired token", + cause, + }), + }) + + return { + type: "user" as const, + workosUserId: decodeWorkOsUserId(String(claims.sub)), + workosOrganizationId: + typeof claims.org_id === "string" + ? decodeWorkOsOrganizationId(claims.org_id) + : null, + role: claims.role === "admin" ? "admin" : "member", + } + }) + + const validateToken = ( + token: string, + ): EffectType.Effect< + AuthenticatedClient, + InvalidTokenFormatError | JwtValidationError | BotTokenValidationError | ConfigError, + HttpClientType.HttpClient + > => { + if (token.startsWith("hzl_bot_")) { + return validateBotToken(token) + } + if (token.split(".").length === 3) { + return validateJwt(token) + } + return Effect.fail( + new InvalidTokenFormatError({ + message: "Invalid token format", + }), + ) + } + + return { + messageActorRuntime: ManagedRuntime.make( + Layer.mergeAll( + FetchHttpClient.layer, + Layer.succeed( + TokenValidationService, + TokenValidationService.of({ + validateBotToken, + validateJwt, + validateToken, + }), + ), + ), + ), + } + }) const mod = await import("./message-actor.ts") - return (mod.messageActor as any).config.createConnState as ( + return (mod.messageActor as { + config: { + createConnState: ( + context: unknown, + params: { token?: string }, + ) => Promise + } + }).config.createConnState as ( context: unknown, params: { token?: string }, - ) => Promise + ) => Promise } afterEach(() => { @@ -44,9 +215,9 @@ afterEach(() => { vi.restoreAllMocks() }) -const BOT_USER_ID = "00000000-0000-0000-0000-000000000011" -const BOT_ID = "00000000-0000-0000-0000-000000000022" -const BOT_ORG_ID = "00000000-0000-0000-0000-000000000033" +const BOT_USER_ID = "00000000-0000-4000-8000-000000000011" +const BOT_ID = "00000000-0000-4000-8000-000000000022" +const BOT_ORG_ID = "00000000-0000-4000-8000-000000000033" describe("messageActor.createConnState", () => { it("returns invalid_token user error for invalid token format", async () => { @@ -155,7 +326,7 @@ describe("messageActor.createConnState", () => { vi.stubGlobal( "fetch", - vi.fn(async (input: string | URL | Request, init?: RequestInit) => { + vi.fn(async (input: Parameters[0], init?: Parameters[1]) => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url @@ -166,7 +337,7 @@ describe("messageActor.createConnState", () => { }) } - return originalFetch(input as any, init) + return originalFetch(input, init) }), ) diff --git a/packages/actors/src/auth/config-service.test.ts b/packages/actors/src/auth/config-service.test.ts index 67ce0869e..934c02f74 100644 --- a/packages/actors/src/auth/config-service.test.ts +++ b/packages/actors/src/auth/config-service.test.ts @@ -1,4 +1,4 @@ -import { Effect, Option, Redacted } from "effect" +import { ConfigProvider, Effect, Option, Redacted } from "effect" import { afterEach, describe, expect, it } from "vitest" import { TokenValidationConfigService } from "./config-service" @@ -20,9 +20,13 @@ const resetEnv = () => { } } -const loadConfig = Effect.gen(function* () { - return yield* TokenValidationConfigService -}).pipe(Effect.provide(TokenValidationConfigService.layer)) +const loadConfig = () => + Effect.gen(function* () { + return yield* TokenValidationConfigService + }).pipe( + Effect.provide(TokenValidationConfigService.layer), + Effect.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(process.env))), + ) afterEach(() => { resetEnv() @@ -37,7 +41,7 @@ describe("TokenValidationConfigService", () => { delete process.env.VITE_API_BASE_URL delete process.env.INTERNAL_SECRET - const config = await Effect.runPromise(loadConfig) + const config = await Effect.runPromise(loadConfig()) expect(Option.isNone(config.workosClientId)).toBe(true) expect(Option.isNone(config.backendUrl)).toBe(true) @@ -49,7 +53,7 @@ describe("TokenValidationConfigService", () => { process.env.BACKEND_URL = "https://backend.example.com" process.env.INTERNAL_SECRET = "super-secret" - const config = await Effect.runPromise(loadConfig) + const config = await Effect.runPromise(loadConfig()) expect(Option.isSome(config.workosClientId)).toBe(true) expect(Option.isSome(config.backendUrl)).toBe(true) diff --git a/packages/actors/src/auth/config-service.ts b/packages/actors/src/auth/config-service.ts index aabd1f720..b691fd1d0 100644 --- a/packages/actors/src/auth/config-service.ts +++ b/packages/actors/src/auth/config-service.ts @@ -14,7 +14,7 @@ export interface TokenValidationConfig { readonly internalSecret: Option.Option } -const optionalValue = (effect: Effect.Effect) => effect.pipe(Effect.option) +const optionalValue = (effect: Effect.Effect) => effect.pipe(Effect.option) /** * Service for loading and providing token validation configuration. diff --git a/packages/actors/src/auth/jwks-service.test.ts b/packages/actors/src/auth/jwks-service.test.ts index 928b2dccc..39e52b4b0 100644 --- a/packages/actors/src/auth/jwks-service.test.ts +++ b/packages/actors/src/auth/jwks-service.test.ts @@ -1,4 +1,4 @@ -import { Effect, Result } from "effect" +import { ConfigProvider, Effect, Result } from "effect" import { afterEach, describe, expect, it } from "vitest" import { JwksService } from "./jwks-service" @@ -32,7 +32,11 @@ describe("JwksService", () => { Effect.gen(function* () { const service = yield* JwksService return yield* service.getJwks() - }).pipe(Effect.provide(JwksService.layer), Effect.result), + }).pipe( + Effect.provide(JwksService.layer), + Effect.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(process.env))), + Effect.result, + ), ) expect(Result.isFailure(result)).toBe(true) @@ -50,7 +54,10 @@ describe("JwksService", () => { const firstJwks = yield* service.getJwks() const secondJwks = yield* service.getJwks() return [firstJwks, secondJwks] as const - }).pipe(Effect.provide(JwksService.layer)), + }).pipe( + Effect.provide(JwksService.layer), + Effect.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(process.env))), + ), ) expect(typeof first).toBe("function") diff --git a/packages/actors/src/effect/runtime.ts b/packages/actors/src/effect/runtime.ts index a4976f00b..faa9c74ae 100644 --- a/packages/actors/src/effect/runtime.ts +++ b/packages/actors/src/effect/runtime.ts @@ -1,7 +1,11 @@ import { FetchHttpClient } from "effect/unstable/http" -import { Layer, ManagedRuntime } from "effect" +import { ConfigProvider, Layer, ManagedRuntime } from "effect" import { TokenValidationLive } from "../auth" -const MessageActorRuntimeLayer = Layer.mergeAll(TokenValidationLive, FetchHttpClient.layer) +const MessageActorRuntimeLayer = Layer.mergeAll( + TokenValidationLive, + FetchHttpClient.layer, + ConfigProvider.layer(ConfigProvider.fromEnv()), +) export const messageActorRuntime = ManagedRuntime.make(MessageActorRuntimeLayer) diff --git a/packages/auth/src/consumers/backend-auth.test.ts b/packages/auth/src/consumers/backend-auth.test.ts index 95240ddc1..41dfc3adf 100644 --- a/packages/auth/src/consumers/backend-auth.test.ts +++ b/packages/auth/src/consumers/backend-auth.test.ts @@ -9,7 +9,7 @@ import { } from "./backend-auth.ts" import type { UserId, WorkOSUserId } from "@hazel/schema" -const MOCK_USER_ID = "00000000-0000-0000-0000-000000000001" as UserId +const MOCK_USER_ID = "00000000-0000-4000-8000-000000000001" as UserId // ===== Mock UserRepo Factory ===== @@ -91,10 +91,10 @@ describe("BackendAuth", () => { it.effect("decodes internal organization IDs from WorkOS externalId", () => Effect.gen(function* () { const orgId = yield* decodeInternalOrganizationIdFromWorkOS( - "00000000-0000-0000-0000-000000000099", + "00000000-0000-4000-8000-000000000099", ) - expect(orgId).toBe("00000000-0000-0000-0000-000000000099") + expect(orgId).toBe("00000000-0000-4000-8000-000000000099") }), ) diff --git a/packages/auth/src/consumers/backend-auth.ts b/packages/auth/src/consumers/backend-auth.ts index 26c90d49f..d172401f8 100644 --- a/packages/auth/src/consumers/backend-auth.ts +++ b/packages/auth/src/consumers/backend-auth.ts @@ -353,7 +353,7 @@ export class BackendAuth extends ServiceMap.Service()("@hazel/auth/ static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(WorkOSClient.layer)) /** Mock user ID - a valid UUID */ - static readonly mockUserId = "00000000-0000-0000-0000-000000000001" as UserId + static readonly mockUserId = "00000000-0000-4000-8000-000000000001" as UserId /** Default mock user for tests */ static mockUser = () => ({ diff --git a/packages/backend-core/src/services/workos-sync.test.ts b/packages/backend-core/src/services/workos-sync.test.ts index eab87246c..84384c17b 100644 --- a/packages/backend-core/src/services/workos-sync.test.ts +++ b/packages/backend-core/src/services/workos-sync.test.ts @@ -10,11 +10,11 @@ import { describe("WorkOSSync helpers", () => { it("decodes valid internal organization IDs from WorkOS external IDs", async () => { const orgId = await Effect.runPromise( - decodeInternalOrganizationId("00000000-0000-0000-0000-000000000055"), + decodeInternalOrganizationId("00000000-0000-4000-8000-000000000055"), ) expect(orgId).toBe( - "00000000-0000-0000-0000-000000000055" as Schema.Schema.Type, + "00000000-0000-4000-8000-000000000055" as Schema.Schema.Type, ) }) diff --git a/packages/domain/src/rpc/channels.ts b/packages/domain/src/rpc/channels.ts index 354b54bba..0aa63dcd4 100644 --- a/packages/domain/src/rpc/channels.ts +++ b/packages/domain/src/rpc/channels.ts @@ -71,10 +71,9 @@ export class CreateThreadRequest extends Schema.Class("Crea * Uses jsonCreate which includes optional id for optimistic updates. * Extended with addAllMembers option to auto-add all organization members. */ -export const CreateChannelRequest = Schema.Struct({ - ...(Channel.Model.jsonCreate as any).fields, - addAllMembers: Schema.optional(Schema.Boolean), -}) +export const CreateChannelRequest = Channel.Model.jsonCreate.pipe( + Schema.fieldsAssign({ addAllMembers: Schema.optional(Schema.Boolean) }), +) export class ChannelRpcs extends RpcGroup.make( /** diff --git a/vitest.config.ts b/vitest.config.ts index b97c771ab..2f23c24e2 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,7 +2,15 @@ import { defineConfig } from "vitest/config" export default defineConfig({ test: { - projects: ["packages/*", "apps/*", "libs/*", "!apps/bot-gateway"], + projects: [ + "packages/*", + "apps/backend", + "apps/electric-proxy", + "apps/link-preview-worker", + "apps/web", + "libs/*", + "!apps/bot-gateway", + ], coverage: { reporter: ["text", "json-summary", "json"], reportOnFailure: true, From 145db9ffb085a0ee050f65a27b9f71934627d1e4 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 16 Mar 2026 21:03:14 +0100 Subject: [PATCH 25/34] format --- .../src/policies/attachment-policy.test.ts | 107 +- .../policies/channel-member-policy.test.ts | 55 +- .../integration-connection-policy.test.ts | 11 +- .../src/policies/invitation-policy.test.ts | 15 +- .../src/policies/message-policy.test.ts | 87 +- .../policies/message-reaction-policy.test.ts | 92 +- .../src/policies/notification-policy.test.ts | 56 +- .../organization-member-policy.test.ts | 25 +- .../policies/pinned-message-policy.test.ts | 49 +- .../src/policies/policy-test-helpers.ts | 47 +- .../policies/typing-indicator-policy.test.ts | 49 +- apps/backend/src/routes/auth.http.test.ts | 164 +- .../src/routes/bot-commands.http.test.ts | 6 +- apps/backend/src/routes/bot-commands.sse.ts | 9 +- apps/backend/src/rpc/handlers/channels.ts | 19 +- .../backend/src/rpc/handlers/organizations.ts | 5 +- apps/backend/src/rpc/middleware/auth.test.ts | 23 +- .../src/services/bot-gateway-service.test.ts | 30 +- .../chat-sync-attribution-reconciler.test.ts | 4 +- .../chat-sync/chat-sync-core-worker.ts | 4217 +++++++++-------- .../discord-gateway-service.dispatch.test.ts | 48 +- .../chat-sync/discord-gateway-shared.ts | 126 +- .../chat-sync/discord-sync-worker.test.ts | 36 +- .../connect-conversation-service.test.ts | 430 +- .../message-outbox-dispatcher.test.ts | 11 +- .../backend/src/services/org-resolver.test.ts | 66 +- apps/backend/src/test/effect-helpers.ts | 5 +- .../src/test/message-outbox-repo.test.ts | 6 +- apps/web/src/atoms/desktop-auth.ts | 464 +- .../components/chat/pinned-messages-modal.tsx | 10 +- .../components/onboarding/org-setup-step.tsx | 12 +- apps/web/src/components/theme-provider.tsx | 4 +- apps/web/src/lib/auth-token.ts | 15 +- .../channels/$channelId/settings/connect.tsx | 2 +- .../_app/$orgSlug/settings/custom-emojis.tsx | 5 +- apps/web/src/utils/status.ts | 4 +- .../src/AtomTanStackDB.result.test.ts | 6 +- .../src/AtomTanStackDB.subscription.test.ts | 6 +- .../src/AtomTanStackDB.timing.test.ts | 6 +- .../message-actor.create-conn-state.test.ts | 26 +- 40 files changed, 3254 insertions(+), 3104 deletions(-) diff --git a/apps/backend/src/policies/attachment-policy.test.ts b/apps/backend/src/policies/attachment-policy.test.ts index fdc5335e1..dd06e818c 100644 --- a/apps/backend/src/policies/attachment-policy.test.ts +++ b/apps/backend/src/policies/attachment-policy.test.ts @@ -26,60 +26,75 @@ const MESSAGE_AUTHOR_ID = "00000000-0000-4000-8000-000000000836" as UserId const makeAttachmentRepoLayer = ( attachments: Record, ) => - Layer.succeed(AttachmentRepo, serviceShape({ - with: ( - id: AttachmentId, - f: (attachment: { uploadedBy: UserId; messageId: MessageId | null }) => Effect.Effect, - ) => { - const attachment = attachments[id] - if (!attachment) { - return Effect.fail(makeEntityNotFound("Attachment")) - } - return f(attachment) - }, - })) + Layer.succeed( + AttachmentRepo, + serviceShape({ + with: ( + id: AttachmentId, + f: (attachment: { + uploadedBy: UserId + messageId: MessageId | null + }) => Effect.Effect, + ) => { + const attachment = attachments[id] + if (!attachment) { + return Effect.fail(makeEntityNotFound("Attachment")) + } + return f(attachment) + }, + }), + ) const makeMessageRepoLayer = (messages: Record) => - Layer.succeed(MessageRepo, serviceShape({ - with: ( - id: MessageId, - f: (message: { authorId: UserId; channelId: ChannelId }) => Effect.Effect, - ) => { - const message = messages[id] - if (!message) { - return Effect.fail(makeEntityNotFound("Message")) - } - return f(message) - }, - })) + Layer.succeed( + MessageRepo, + serviceShape({ + with: ( + id: MessageId, + f: (message: { authorId: UserId; channelId: ChannelId }) => Effect.Effect, + ) => { + const message = messages[id] + if (!message) { + return Effect.fail(makeEntityNotFound("Message")) + } + return f(message) + }, + }), + ) const makeChannelRepoLayer = ( channels: Record, ) => - Layer.succeed(ChannelRepo, serviceShape({ - with: ( - id: ChannelId, - f: (channel: { - organizationId: OrganizationId - type: string - id: ChannelId - }) => Effect.Effect, - ) => { - const channel = channels[id] - if (!channel) { - return Effect.fail(makeEntityNotFound("Channel")) - } - return f(channel) - }, - })) + Layer.succeed( + ChannelRepo, + serviceShape({ + with: ( + id: ChannelId, + f: (channel: { + organizationId: OrganizationId + type: string + id: ChannelId + }) => Effect.Effect, + ) => { + const channel = channels[id] + if (!channel) { + return Effect.fail(makeEntityNotFound("Channel")) + } + return f(channel) + }, + }), + ) const makeChannelMemberRepoLayer = (memberships: Record) => - Layer.succeed(ChannelMemberRepo, serviceShape({ - findByChannelAndUser: (channelId: ChannelId, userId: UserId) => { - const key = `${channelId}:${userId}` - return Effect.succeed(memberships[key] ? Option.some({ channelId, userId }) : Option.none()) - }, - })) + Layer.succeed( + ChannelMemberRepo, + serviceShape({ + findByChannelAndUser: (channelId: ChannelId, userId: UserId) => { + const key = `${channelId}:${userId}` + return Effect.succeed(memberships[key] ? Option.some({ channelId, userId }) : Option.none()) + }, + }), + ) const makePolicyLayer = (opts: { members?: Record diff --git a/apps/backend/src/policies/channel-member-policy.test.ts b/apps/backend/src/policies/channel-member-policy.test.ts index b9dd6535a..b932551cf 100644 --- a/apps/backend/src/policies/channel-member-policy.test.ts +++ b/apps/backend/src/policies/channel-member-policy.test.ts @@ -37,31 +37,40 @@ const makeChannelMemberRepoLayer = ( channelMembers: Record, membershipsByChannelAndUser: Record = {}, ) => - Layer.succeed(ChannelMemberRepo, serviceShape({ - with: (id: ChannelMemberId, f: (member: ChannelMemberEntry) => Effect.Effect) => { - const member = channelMembers[id] - if (!member) { - return Effect.fail(makeEntityNotFound("ChannelMember")) - } - return f(member) - }, - findByChannelAndUser: (channelId: ChannelId, userId: UserId) => { - const key = `${channelId}:${userId}` - const entry = membershipsByChannelAndUser[key] - return Effect.succeed(entry ? Option.some(entry) : Option.none()) - }, - })) + Layer.succeed( + ChannelMemberRepo, + serviceShape({ + with: ( + id: ChannelMemberId, + f: (member: ChannelMemberEntry) => Effect.Effect, + ) => { + const member = channelMembers[id] + if (!member) { + return Effect.fail(makeEntityNotFound("ChannelMember")) + } + return f(member) + }, + findByChannelAndUser: (channelId: ChannelId, userId: UserId) => { + const key = `${channelId}:${userId}` + const entry = membershipsByChannelAndUser[key] + return Effect.succeed(entry ? Option.some(entry) : Option.none()) + }, + }), + ) const makeChannelRepoLayer = (channels: Record) => - Layer.succeed(ChannelRepo, serviceShape({ - with: (id: ChannelId, f: (channel: ChannelEntry) => Effect.Effect) => { - const channel = channels[id] - if (!channel) { - return Effect.fail(makeEntityNotFound("Channel")) - } - return f(channel) - }, - })) + Layer.succeed( + ChannelRepo, + serviceShape({ + with: (id: ChannelId, f: (channel: ChannelEntry) => Effect.Effect) => { + const channel = channels[id] + if (!channel) { + return Effect.fail(makeEntityNotFound("Channel")) + } + return f(channel) + }, + }), + ) const makePolicyLayer = (opts: { members: Record diff --git a/apps/backend/src/policies/integration-connection-policy.test.ts b/apps/backend/src/policies/integration-connection-policy.test.ts index 69729d788..1d2b2468b 100644 --- a/apps/backend/src/policies/integration-connection-policy.test.ts +++ b/apps/backend/src/policies/integration-connection-policy.test.ts @@ -2,17 +2,14 @@ import { describe, expect, it } from "@effect/vitest" import { UnauthorizedError } from "@hazel/domain" import { Result, Layer } from "effect" import { IntegrationConnectionPolicy } from "./integration-connection-policy.ts" -import { - makeActor, - makeOrgResolverLayer, - runWithActorEither, - TEST_ORG_ID, -} from "./policy-test-helpers.ts" +import { makeActor, makeOrgResolverLayer, runWithActorEither, TEST_ORG_ID } from "./policy-test-helpers.ts" type Role = "admin" | "member" | "owner" const makePolicyLayer = (members: Record) => - Layer.effect(IntegrationConnectionPolicy, IntegrationConnectionPolicy.make).pipe(Layer.provide(makeOrgResolverLayer(members))) + Layer.effect(IntegrationConnectionPolicy, IntegrationConnectionPolicy.make).pipe( + Layer.provide(makeOrgResolverLayer(members)), + ) describe("IntegrationConnectionPolicy", () => { it("allows select for any org member", async () => { diff --git a/apps/backend/src/policies/invitation-policy.test.ts b/apps/backend/src/policies/invitation-policy.test.ts index 3c3c97582..eca787c8c 100644 --- a/apps/backend/src/policies/invitation-policy.test.ts +++ b/apps/backend/src/policies/invitation-policy.test.ts @@ -42,12 +42,15 @@ const makeInvitationRepoLayer = ( } as ServiceMap.Service.Shape) const makeUserRepoLayer = (users: Record) => - Layer.succeed(UserRepo, serviceShape({ - findById: (id: UserId) => { - const user = users[id] - return Effect.succeed(user ? Option.some(user) : Option.none()) - }, - })) + Layer.succeed( + UserRepo, + serviceShape({ + findById: (id: UserId) => { + const user = users[id] + return Effect.succeed(user ? Option.some(user) : Option.none()) + }, + }), + ) const makePolicyLayer = ( members: Record, diff --git a/apps/backend/src/policies/message-policy.test.ts b/apps/backend/src/policies/message-policy.test.ts index 921fa4e32..9caad3609 100644 --- a/apps/backend/src/policies/message-policy.test.ts +++ b/apps/backend/src/policies/message-policy.test.ts @@ -27,51 +27,60 @@ const MISSING_MESSAGE_ID = "00000000-0000-4000-8000-000000000899" as MessageId const makeChannelRepoLayer = ( channels: Record, ) => - Layer.succeed(ChannelRepo, serviceShape({ - findById: (id: ChannelId) => { - const channel = channels[id] - return Effect.succeed(channel ? Option.some(channel) : Option.none()) - }, - with: ( - id: ChannelId, - f: (channel: { - organizationId: OrganizationId - type: string - id: ChannelId - }) => Effect.Effect, - ) => { - const channel = channels[id] - if (!channel) { - return Effect.fail(makeEntityNotFound("Channel")) - } - return f(channel) - }, - })) + Layer.succeed( + ChannelRepo, + serviceShape({ + findById: (id: ChannelId) => { + const channel = channels[id] + return Effect.succeed(channel ? Option.some(channel) : Option.none()) + }, + with: ( + id: ChannelId, + f: (channel: { + organizationId: OrganizationId + type: string + id: ChannelId + }) => Effect.Effect, + ) => { + const channel = channels[id] + if (!channel) { + return Effect.fail(makeEntityNotFound("Channel")) + } + return f(channel) + }, + }), + ) /** * Creates a MessageRepo mock with a `with` method. */ const makeMessageRepoLayer = (messages: Record) => - Layer.succeed(MessageRepo, serviceShape({ - findById: (id: MessageId) => { - const message = messages[id] - return Effect.succeed(message ? Option.some(message) : Option.none()) - }, - with: ( - id: MessageId, - f: (message: { authorId: UserId; channelId: ChannelId }) => Effect.Effect, - ) => { - const message = messages[id] - if (!message) { - return Effect.fail(makeEntityNotFound("Message")) - } - return f(message) - }, - })) + Layer.succeed( + MessageRepo, + serviceShape({ + findById: (id: MessageId) => { + const message = messages[id] + return Effect.succeed(message ? Option.some(message) : Option.none()) + }, + with: ( + id: MessageId, + f: (message: { authorId: UserId; channelId: ChannelId }) => Effect.Effect, + ) => { + const message = messages[id] + if (!message) { + return Effect.fail(makeEntityNotFound("Message")) + } + return f(message) + }, + }), + ) -const emptyChannelMemberRepoLayer = Layer.succeed(ChannelMemberRepo, serviceShape({ - findByChannelAndUser: (_channelId: ChannelId, _userId: UserId) => Effect.succeed(Option.none()), -})) +const emptyChannelMemberRepoLayer = Layer.succeed( + ChannelMemberRepo, + serviceShape({ + findByChannelAndUser: (_channelId: ChannelId, _userId: UserId) => Effect.succeed(Option.none()), + }), +) /** * Builds the full layer stack for MessagePolicy tests. diff --git a/apps/backend/src/policies/message-reaction-policy.test.ts b/apps/backend/src/policies/message-reaction-policy.test.ts index a397ca77a..92177306d 100644 --- a/apps/backend/src/policies/message-reaction-policy.test.ts +++ b/apps/backend/src/policies/message-reaction-policy.test.ts @@ -39,55 +39,73 @@ type MessageData = { channelId: ChannelId } type ChannelData = { organizationId: OrganizationId; type: string; id: string } const makeReactionRepoLayer = (reactions: Record) => - Layer.succeed(MessageReactionRepo, serviceShape({ - with: (id: MessageReactionId, f: (r: ReactionData) => Effect.Effect) => { - const reaction = reactions[id] - if (!reaction) return Effect.fail(makeEntityNotFound("MessageReaction")) - return f(reaction) - }, - })) + Layer.succeed( + MessageReactionRepo, + serviceShape({ + with: (id: MessageReactionId, f: (r: ReactionData) => Effect.Effect) => { + const reaction = reactions[id] + if (!reaction) return Effect.fail(makeEntityNotFound("MessageReaction")) + return f(reaction) + }, + }), + ) const makeMessageRepoLayer = (messages: Record) => - Layer.succeed(MessageRepo, serviceShape({ - with: (id: MessageId, f: (m: MessageData) => Effect.Effect) => { - const message = messages[id] - if (!message) return Effect.fail(makeEntityNotFound("Message")) - return f(message) - }, - findById: (id: MessageId) => { - const message = messages[id] - return Effect.succeed(message ? Option.some(message) : Option.none()) - }, - })) + Layer.succeed( + MessageRepo, + serviceShape({ + with: (id: MessageId, f: (m: MessageData) => Effect.Effect) => { + const message = messages[id] + if (!message) return Effect.fail(makeEntityNotFound("Message")) + return f(message) + }, + findById: (id: MessageId) => { + const message = messages[id] + return Effect.succeed(message ? Option.some(message) : Option.none()) + }, + }), + ) const makeChannelRepoLayer = (channels: Record) => - Layer.succeed(ChannelRepo, serviceShape({ - findById: (id: ChannelId) => { - const channel = channels[id] - return Effect.succeed(channel ? Option.some(channel) : Option.none()) - }, - })) + Layer.succeed( + ChannelRepo, + serviceShape({ + findById: (id: ChannelId) => { + const channel = channels[id] + return Effect.succeed(channel ? Option.some(channel) : Option.none()) + }, + }), + ) const makeOrgMemberRepoLayer = (orgMembers: Record) => - Layer.succeed(OrganizationMemberRepo, serviceShape({ - findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { - const role = orgMembers[`${organizationId}:${userId}`] - return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) - }, - })) + Layer.succeed( + OrganizationMemberRepo, + serviceShape({ + findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { + const role = orgMembers[`${organizationId}:${userId}`] + return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) + }, + }), + ) -const emptyChannelMemberRepoLayer = Layer.succeed(ChannelMemberRepo, serviceShape({ - findByChannelAndUser: (_channelId: ChannelId, _userId: UserId) => Effect.succeed(Option.none()), -})) +const emptyChannelMemberRepoLayer = Layer.succeed( + ChannelMemberRepo, + serviceShape({ + findByChannelAndUser: (_channelId: ChannelId, _userId: UserId) => Effect.succeed(Option.none()), + }), +) -const emptyMessageRepoLayer = Layer.succeed(MessageRepo, serviceShape({ - findById: (_id: MessageId) => Effect.succeed(Option.none()), -})) +const emptyMessageRepoLayer = Layer.succeed( + MessageRepo, + serviceShape({ + findById: (_id: MessageId) => Effect.succeed(Option.none()), + }), +) const connectConversationServiceLayer = Layer.succeed( ConnectConversationService, serviceShape({ - canAccessConversation: () => Effect.succeed(false), + canAccessConversation: () => Effect.succeed(false), }), ) diff --git a/apps/backend/src/policies/notification-policy.test.ts b/apps/backend/src/policies/notification-policy.test.ts index 5de8b83cb..836fe961e 100644 --- a/apps/backend/src/policies/notification-policy.test.ts +++ b/apps/backend/src/policies/notification-policy.test.ts @@ -24,33 +24,39 @@ type NotificationData = { memberId: OrganizationMemberId } type MemberData = { userId: UserId; organizationId: OrganizationId; role: string } const makeNotificationRepoLayer = (notifications: Record) => - Layer.succeed(NotificationRepo, serviceShape({ - with: ( - id: NotificationId, - f: (notification: NotificationData) => Effect.Effect, - ) => { - const notification = notifications[id] - if (!notification) return Effect.fail(makeEntityNotFound("Notification")) - return f(notification) - }, - })) + Layer.succeed( + NotificationRepo, + serviceShape({ + with: ( + id: NotificationId, + f: (notification: NotificationData) => Effect.Effect, + ) => { + const notification = notifications[id] + if (!notification) return Effect.fail(makeEntityNotFound("Notification")) + return f(notification) + }, + }), + ) const makeOrgMemberRepoLayer = (members: Record, orgMembers: Record) => - Layer.succeed(OrganizationMemberRepo, serviceShape({ - with: (id: OrganizationMemberId, f: (m: MemberData) => Effect.Effect) => { - const member = members[id] - if (!member) return Effect.fail(makeEntityNotFound("OrganizationMember")) - return f(member) - }, - findById: (id: OrganizationMemberId) => { - const member = members[id] - return Effect.succeed(member ? Option.some(member) : Option.none()) - }, - findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { - const role = orgMembers[`${organizationId}:${userId}`] - return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) - }, - })) + Layer.succeed( + OrganizationMemberRepo, + serviceShape({ + with: (id: OrganizationMemberId, f: (m: MemberData) => Effect.Effect) => { + const member = members[id] + if (!member) return Effect.fail(makeEntityNotFound("OrganizationMember")) + return f(member) + }, + findById: (id: OrganizationMemberId) => { + const member = members[id] + return Effect.succeed(member ? Option.some(member) : Option.none()) + }, + findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { + const role = orgMembers[`${organizationId}:${userId}`] + return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) + }, + }), + ) const makePolicyLayer = ( notifications: Record, diff --git a/apps/backend/src/policies/organization-member-policy.test.ts b/apps/backend/src/policies/organization-member-policy.test.ts index a2bd1f117..a6ae7c2e5 100644 --- a/apps/backend/src/policies/organization-member-policy.test.ts +++ b/apps/backend/src/policies/organization-member-policy.test.ts @@ -23,17 +23,20 @@ const OWNER_USER_ID = "00000000-0000-4000-8000-000000000854" as UserId type MemberData = { userId: UserId; organizationId: OrganizationId; role: string } const makeOrgMemberRepoLayer = (membersById: Record, orgMembers: Record) => - Layer.succeed(OrganizationMemberRepo, serviceShape({ - with: (id: OrganizationMemberId, f: (m: MemberData) => Effect.Effect) => { - const member = membersById[id] - if (!member) return Effect.fail(makeEntityNotFound("OrganizationMember")) - return f(member) - }, - findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { - const role = orgMembers[`${organizationId}:${userId}`] - return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) - }, - })) + Layer.succeed( + OrganizationMemberRepo, + serviceShape({ + with: (id: OrganizationMemberId, f: (m: MemberData) => Effect.Effect) => { + const member = membersById[id] + if (!member) return Effect.fail(makeEntityNotFound("OrganizationMember")) + return f(member) + }, + findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { + const role = orgMembers[`${organizationId}:${userId}`] + return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) + }, + }), + ) const makePolicyLayer = (membersById: Record, orgMembers: Record) => Layer.effect(OrganizationMemberPolicy, OrganizationMemberPolicy.make).pipe( diff --git a/apps/backend/src/policies/pinned-message-policy.test.ts b/apps/backend/src/policies/pinned-message-policy.test.ts index edebc0a65..c3fc0c1fc 100644 --- a/apps/backend/src/policies/pinned-message-policy.test.ts +++ b/apps/backend/src/policies/pinned-message-policy.test.ts @@ -24,30 +24,39 @@ type ChannelData = { organizationId: OrganizationId; type: string; id: string } type PinnedData = { pinnedBy: UserId; channelId: ChannelId } const makePinnedMessageRepoLayer = (pinnedMessages: Record) => - Layer.succeed(PinnedMessageRepo, serviceShape({ - with: (id: PinnedMessageId, f: (pm: PinnedData) => Effect.Effect) => { - const pm = pinnedMessages[id] - if (!pm) return Effect.fail(makeEntityNotFound("PinnedMessage")) - return f(pm) - }, - })) + Layer.succeed( + PinnedMessageRepo, + serviceShape({ + with: (id: PinnedMessageId, f: (pm: PinnedData) => Effect.Effect) => { + const pm = pinnedMessages[id] + if (!pm) return Effect.fail(makeEntityNotFound("PinnedMessage")) + return f(pm) + }, + }), + ) const makeChannelRepoLayer = (channels: Record) => - Layer.succeed(ChannelRepo, serviceShape({ - with: (id: ChannelId, f: (ch: ChannelData) => Effect.Effect) => { - const ch = channels[id] - if (!ch) return Effect.fail(makeEntityNotFound("Channel")) - return f(ch) - }, - })) + Layer.succeed( + ChannelRepo, + serviceShape({ + with: (id: ChannelId, f: (ch: ChannelData) => Effect.Effect) => { + const ch = channels[id] + if (!ch) return Effect.fail(makeEntityNotFound("Channel")) + return f(ch) + }, + }), + ) const makeOrgMemberRepoLayer = (orgMembers: Record) => - Layer.succeed(OrganizationMemberRepo, serviceShape({ - findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { - const role = orgMembers[`${organizationId}:${userId}`] - return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) - }, - })) + Layer.succeed( + OrganizationMemberRepo, + serviceShape({ + findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { + const role = orgMembers[`${organizationId}:${userId}`] + return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) + }, + }), + ) const makePolicyLayer = ( orgMembers: Record, diff --git a/apps/backend/src/policies/policy-test-helpers.ts b/apps/backend/src/policies/policy-test-helpers.ts index f41054d52..c008b4e60 100644 --- a/apps/backend/src/policies/policy-test-helpers.ts +++ b/apps/backend/src/policies/policy-test-helpers.ts @@ -53,37 +53,46 @@ type Role = "admin" | "member" | "owner" * Creates a mock OrganizationMemberRepo layer for testing. */ export const makeOrganizationMemberRepoLayer = (members: Record) => - Layer.succeed(OrganizationMemberRepo, serviceShape({ - findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { - const role = members[`${organizationId}:${userId}`] - return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) - }, - })) + Layer.succeed( + OrganizationMemberRepo, + serviceShape({ + findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { + const role = members[`${organizationId}:${userId}`] + return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) + }, + }), + ) /** * Creates a stub repo layer that returns none/empty for all lookups. * Used for OrgResolver dependencies that aren't relevant to a specific test. */ -const emptyChannelRepoLayer = Layer.succeed(ChannelRepo, serviceShape({ - findById: (_id: ChannelId) => Effect.succeed(Option.none()), - with: (_id: ChannelId, _f: (c: any) => Effect.Effect) => - Effect.fail(makeEntityNotFound("Channel")), -})) +const emptyChannelRepoLayer = Layer.succeed( + ChannelRepo, + serviceShape({ + findById: (_id: ChannelId) => Effect.succeed(Option.none()), + with: (_id: ChannelId, _f: (c: any) => Effect.Effect) => + Effect.fail(makeEntityNotFound("Channel")), + }), +) const emptyChannelMemberRepoLayer = Layer.succeed( ChannelMemberRepo, serviceShape({ - findByChannelAndUser: (_channelId: ChannelId, _userId: UserId) => Effect.succeed(Option.none()), - with: (_id: ChannelMemberId, _f: (c: any) => Effect.Effect) => - Effect.fail(makeEntityNotFound("ChannelMember")), + findByChannelAndUser: (_channelId: ChannelId, _userId: UserId) => Effect.succeed(Option.none()), + with: (_id: ChannelMemberId, _f: (c: any) => Effect.Effect) => + Effect.fail(makeEntityNotFound("ChannelMember")), }), ) -const emptyMessageRepoLayer = Layer.succeed(MessageRepo, serviceShape({ - findById: (_id: MessageId) => Effect.succeed(Option.none()), - with: (_id: MessageId, _f: (c: any) => Effect.Effect) => - Effect.fail(makeEntityNotFound("Message")), -})) +const emptyMessageRepoLayer = Layer.succeed( + MessageRepo, + serviceShape({ + findById: (_id: MessageId) => Effect.succeed(Option.none()), + with: (_id: MessageId, _f: (c: any) => Effect.Effect) => + Effect.fail(makeEntityNotFound("Message")), + }), +) /** * Creates an OrgResolver layer backed by the given member mock. diff --git a/apps/backend/src/policies/typing-indicator-policy.test.ts b/apps/backend/src/policies/typing-indicator-policy.test.ts index b0241b963..3208f77b0 100644 --- a/apps/backend/src/policies/typing-indicator-policy.test.ts +++ b/apps/backend/src/policies/typing-indicator-policy.test.ts @@ -30,28 +30,37 @@ const makeChannelMemberRepoLayer = ( recordsByMemberId: Record, recordsByChannelAndUser: Record, ) => - Layer.succeed(ChannelMemberRepo, serviceShape({ - findByChannelAndUser: (channelId: ChannelId, userId: UserId) => - Effect.succeed(Option.fromNullishOr(recordsByChannelAndUser[`${channelId}:${userId}`])), - with: (id: ChannelMemberId, f: (member: MemberRecord) => Effect.Effect) => { - const member = recordsByMemberId[id] - if (!member) { - return Effect.fail(makeEntityNotFound("ChannelMember")) - } - return f(member) - }, - })) + Layer.succeed( + ChannelMemberRepo, + serviceShape({ + findByChannelAndUser: (channelId: ChannelId, userId: UserId) => + Effect.succeed(Option.fromNullishOr(recordsByChannelAndUser[`${channelId}:${userId}`])), + with: (id: ChannelMemberId, f: (member: MemberRecord) => Effect.Effect) => { + const member = recordsByMemberId[id] + if (!member) { + return Effect.fail(makeEntityNotFound("ChannelMember")) + } + return f(member) + }, + }), + ) const makeTypingIndicatorRepoLayer = (recordsById: Record) => - Layer.succeed(TypingIndicatorRepo, serviceShape({ - with: (id: TypingIndicatorId, f: (indicator: IndicatorRecord) => Effect.Effect) => { - const indicator = recordsById[id] - if (!indicator) { - return Effect.fail(makeEntityNotFound("TypingIndicator")) - } - return f(indicator) - }, - })) + Layer.succeed( + TypingIndicatorRepo, + serviceShape({ + with: ( + id: TypingIndicatorId, + f: (indicator: IndicatorRecord) => Effect.Effect, + ) => { + const indicator = recordsById[id] + if (!indicator) { + return Effect.fail(makeEntityNotFound("TypingIndicator")) + } + return f(indicator) + }, + }), + ) const makePolicyLayer = ( channelMembersById: Record, diff --git a/apps/backend/src/routes/auth.http.test.ts b/apps/backend/src/routes/auth.http.test.ts index 8a62b3615..0fc4efdc9 100644 --- a/apps/backend/src/routes/auth.http.test.ts +++ b/apps/backend/src/routes/auth.http.test.ts @@ -56,73 +56,70 @@ const createMockWorkOSLive = (options?: { shouldFailLogin?: boolean shouldFailGetOrg?: boolean }) => - Layer.succeed( - WorkOS, - ({ - call: (f: (client: WorkOSNodeAPI, signal: AbortSignal) => Promise) => - Effect.tryPromise({ - try: async () => { - const mockClient = { - userManagement: { - getAuthorizationUrl: (params: { clientId: string; state?: string }) => { - if (options?.shouldFailLogin) { - throw new Error("WorkOS API error") - } - return ( - options?.authorizationUrl ?? - `https://workos.com/auth?client_id=${params.clientId}&state=${params.state}` - ) - }, - authenticateWithCode: async () => { - if (options?.shouldFailAuth) { - throw new Error("Authentication failed") - } - return { - user: options?.authenticateResponse?.user ?? { - id: "user_01ABC123", - email: "test@example.com", - firstName: "Test", - lastName: "User", - profilePictureUrl: null, - }, - sealedSession: - options?.authenticateResponse?.sealedSession ?? - "sealed-session-cookie", - organizationId: options?.authenticateResponse?.organizationId, - } - }, - listOrganizationMemberships: async () => ({ - data: [{ role: { slug: "member" } }], - }), + Layer.succeed(WorkOS, { + call: (f: (client: WorkOSNodeAPI, signal: AbortSignal) => Promise) => + Effect.tryPromise({ + try: async () => { + const mockClient = { + userManagement: { + getAuthorizationUrl: (params: { clientId: string; state?: string }) => { + if (options?.shouldFailLogin) { + throw new Error("WorkOS API error") + } + return ( + options?.authorizationUrl ?? + `https://workos.com/auth?client_id=${params.clientId}&state=${params.state}` + ) }, - organizations: { - getOrganization: async (id: string) => { - if (options?.shouldFailGetOrg) { - throw new Error("Org not found") - } - return { - id, - externalId: "org_internal_123", - } - }, - getOrganizationByExternalId: async (externalId: string) => { - if (options?.shouldFailGetOrg) { - throw new Error("Org not found") - } - return { - id: "org_workos_123", - externalId, - } - }, + authenticateWithCode: async () => { + if (options?.shouldFailAuth) { + throw new Error("Authentication failed") + } + return { + user: options?.authenticateResponse?.user ?? { + id: "user_01ABC123", + email: "test@example.com", + firstName: "Test", + lastName: "User", + profilePictureUrl: null, + }, + sealedSession: + options?.authenticateResponse?.sealedSession ?? + "sealed-session-cookie", + organizationId: options?.authenticateResponse?.organizationId, + } }, - } + listOrganizationMemberships: async () => ({ + data: [{ role: { slug: "member" } }], + }), + }, + organizations: { + getOrganization: async (id: string) => { + if (options?.shouldFailGetOrg) { + throw new Error("Org not found") + } + return { + id, + externalId: "org_internal_123", + } + }, + getOrganizationByExternalId: async (externalId: string) => { + if (options?.shouldFailGetOrg) { + throw new Error("Org not found") + } + return { + id: "org_workos_123", + externalId, + } + }, + }, + } - return f(mockClient as unknown as WorkOSNodeAPI, new AbortController().signal) - }, - catch: (cause) => new WorkOSApiError({ cause }), - }), - }) satisfies ServiceMap.Service.Shape, - ) + return f(mockClient as unknown as WorkOSNodeAPI, new AbortController().signal) + }, + catch: (cause) => new WorkOSApiError({ cause }), + }), + } satisfies ServiceMap.Service.Shape) // ===== Mock UserRepo ===== @@ -157,13 +154,16 @@ const createMockUserRepoLive = (options?: { // ===== Mock OrganizationMemberRepo ===== -const MockOrganizationMemberRepoLive = Layer.succeed(OrganizationMemberRepo, serviceShape({ - findByOrgAndUser: (_orgId: OrganizationId, _userId: UserId) => Effect.succeed(Option.none()), - upsertByOrgAndUser: (_membership: Schema.Schema.Type) => - Effect.succeed({ - id: "00000000-0000-4000-8000-000000000099", - }), -})) +const MockOrganizationMemberRepoLive = Layer.succeed( + OrganizationMemberRepo, + serviceShape({ + findByOrgAndUser: (_orgId: OrganizationId, _userId: UserId) => Effect.succeed(Option.none()), + upsertByOrgAndUser: (_membership: Schema.Schema.Type) => + Effect.succeed({ + id: "00000000-0000-4000-8000-000000000099", + }), + }), +) // ===== Test Layer Factory ===== @@ -208,19 +208,19 @@ describe("Auth HTTP Endpoint Logic", () => { }) }) - describe("AuthState schema", () => { - it("creates valid AuthState", () => { - const state = Schema.decodeSync(AuthState)({ returnTo: "/dashboard" }) - expect(state.returnTo).toBe("/dashboard") - }) + describe("AuthState schema", () => { + it("creates valid AuthState", () => { + const state = Schema.decodeSync(AuthState)({ returnTo: "/dashboard" }) + expect(state.returnTo).toBe("/dashboard") + }) - it("serializes and deserializes correctly", () => { - const state = Schema.decodeSync(AuthState)({ returnTo: "/settings/profile" }) - const serialized = JSON.stringify(state) - const parsed = Schema.decodeSync(AuthState)(JSON.parse(serialized)) - expect(parsed.returnTo).toBe("/settings/profile") - }) + it("serializes and deserializes correctly", () => { + const state = Schema.decodeSync(AuthState)({ returnTo: "/settings/profile" }) + const serialized = JSON.stringify(state) + const parsed = Schema.decodeSync(AuthState)(JSON.parse(serialized)) + expect(parsed.returnTo).toBe("/settings/profile") }) + }) describe("Login flow", () => { layer(TestLayer)("authorization URL generation", (it) => { diff --git a/apps/backend/src/routes/bot-commands.http.test.ts b/apps/backend/src/routes/bot-commands.http.test.ts index 4f8b72c18..cd0bf1d8a 100644 --- a/apps/backend/src/routes/bot-commands.http.test.ts +++ b/apps/backend/src/routes/bot-commands.http.test.ts @@ -1,10 +1,6 @@ import { describe, expect, it } from "@effect/vitest" import { Effect, Fiber, Stream } from "effect" -import { - createCommandSseStream, - createSseHeartbeatStream, - type CommandSseRedis, -} from "./bot-commands.sse.ts" +import { createCommandSseStream, createSseHeartbeatStream, type CommandSseRedis } from "./bot-commands.sse.ts" describe("bot command SSE streams", () => { it("emits an immediate heartbeat on connect", () => diff --git a/apps/backend/src/routes/bot-commands.sse.ts b/apps/backend/src/routes/bot-commands.sse.ts index c6daaf1b0..4cc574871 100644 --- a/apps/backend/src/routes/bot-commands.sse.ts +++ b/apps/backend/src/routes/bot-commands.sse.ts @@ -15,9 +15,12 @@ export type CommandSseRedis = { readonly subscribe: ( channel: string, handler: (message: string, channel: string) => void, - ) => Effect.Effect<{ - readonly unsubscribe: Effect.Effect - }, unknown> + ) => Effect.Effect< + { + readonly unsubscribe: Effect.Effect + }, + unknown + > } export const createSseHeartbeatStream = (interval: Duration.Input = HEARTBEAT_INTERVAL) => diff --git a/apps/backend/src/rpc/handlers/channels.ts b/apps/backend/src/rpc/handlers/channels.ts index bc20c0d21..24de42a13 100644 --- a/apps/backend/src/rpc/handlers/channels.ts +++ b/apps/backend/src/rpc/handlers/channels.ts @@ -478,14 +478,17 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( const originalMessageId = originalMessageResult[0]!.id - const clusterUrl = yield* Config.string("CLUSTER_URL").asEffect().pipe( - Effect.mapError(() => - new WorkflowServiceUnavailableError({ - message: "CLUSTER_URL not configured", - cause: "Missing CLUSTER_URL environment variable", - }), - ), - ) + const clusterUrl = yield* Config.string("CLUSTER_URL") + .asEffect() + .pipe( + Effect.mapError( + () => + new WorkflowServiceUnavailableError({ + message: "CLUSTER_URL not configured", + cause: "Missing CLUSTER_URL environment variable", + }), + ), + ) const client = yield* HttpApiClient.make(Cluster.WorkflowApi, { baseUrl: clusterUrl, }) diff --git a/apps/backend/src/rpc/handlers/organizations.ts b/apps/backend/src/rpc/handlers/organizations.ts index 863a6c684..e8b1530dc 100644 --- a/apps/backend/src/rpc/handlers/organizations.ts +++ b/apps/backend/src/rpc/handlers/organizations.ts @@ -44,10 +44,7 @@ const handleOrganizationDbErrors = ( return effect.pipe( Effect.catchIf( (e): e is Extract => Predicate.isTagged(e, "DatabaseError"), - (err): Effect.Effect< - never, - InternalServerError | OrganizationSlugAlreadyExistsError - > => { + (err): Effect.Effect => { const dbErr = err as unknown as { type: string cause: { constraint_name?: string; detail?: string } diff --git a/apps/backend/src/rpc/middleware/auth.test.ts b/apps/backend/src/rpc/middleware/auth.test.ts index ecc79022f..ea0420bd1 100644 --- a/apps/backend/src/rpc/middleware/auth.test.ts +++ b/apps/backend/src/rpc/middleware/auth.test.ts @@ -2,10 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import { Headers } from "effect/unstable/http" import type { SuccessValue } from "effect/unstable/rpc/RpcMiddleware" import { BotRepo, UserRepo } from "@hazel/backend-core" -import { - CurrentUser, - type CurrentUser as CurrentUserNamespace, -} from "@hazel/domain" +import { CurrentUser, type CurrentUser as CurrentUserNamespace } from "@hazel/domain" import type { UserId } from "@hazel/schema" import { Effect, Layer, Option, Ref, Result, ServiceMap } from "effect" import { AuthMiddleware, AuthMiddlewareLive } from "./auth.ts" @@ -141,12 +138,10 @@ const runAuth = ( Effect.provide(AuthMiddlewareLive), Effect.provide(overrides.sessionManager ?? makeSessionManagerLayer(makeCurrentUser())), Effect.provide( - overrides.botRepo ?? - makeBotRepoLayer(() => Effect.succeed(Option.none())), + overrides.botRepo ?? makeBotRepoLayer(() => Effect.succeed(Option.none())), ), Effect.provide( - overrides.userRepo ?? - makeUserRepoLayer(() => Effect.succeed(Option.none())), + overrides.userRepo ?? makeUserRepoLayer(() => Effect.succeed(Option.none())), ), Effect.result, ), @@ -175,9 +170,7 @@ describe("AuthMiddlewareLive", () => { expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { const failureTag = - typeof result.failure === "object" && - result.failure !== null && - "_tag" in result.failure + typeof result.failure === "object" && result.failure !== null && "_tag" in result.failure ? result.failure._tag : "unhandled" expect(failureTag).toBe("SessionNotProvidedError") @@ -194,9 +187,7 @@ describe("AuthMiddlewareLive", () => { ), ), userRepo: makeUserRepoLayer((id) => - Effect.succeed( - id === BOT_USER_ID ? Option.some(USER_RECORD) : Option.none(), - ), + Effect.succeed(id === BOT_USER_ID ? Option.some(USER_RECORD) : Option.none()), ), }) @@ -218,9 +209,7 @@ describe("AuthMiddlewareLive", () => { expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) { const failureTag = - typeof result.failure === "object" && - result.failure !== null && - "_tag" in result.failure + typeof result.failure === "object" && result.failure !== null && "_tag" in result.failure ? result.failure._tag : "unhandled" expect(failureTag).toBe("InvalidBearerTokenError") diff --git a/apps/backend/src/services/bot-gateway-service.test.ts b/apps/backend/src/services/bot-gateway-service.test.ts index ad1828e07..ab5632552 100644 --- a/apps/backend/src/services/bot-gateway-service.test.ts +++ b/apps/backend/src/services/bot-gateway-service.test.ts @@ -20,20 +20,26 @@ const TestConfigLive = configLayer({ }) const makeBotInstallationRepoLayer = (botIds: ReadonlyArray) => - Layer.succeed(BotInstallationRepo, serviceShape({ - getBotIdsForOrg: () => Effect.succeed([...botIds]), - })) + Layer.succeed( + BotInstallationRepo, + serviceShape({ + getBotIdsForOrg: () => Effect.succeed([...botIds]), + }), + ) const makeChannelRepoLayer = (organizationId: OrganizationId) => - Layer.succeed(ChannelRepo, serviceShape({ - findById: (id: ChannelId) => - Effect.succeed( - Option.some({ - id, - organizationId, - }), - ), - })) + Layer.succeed( + ChannelRepo, + serviceShape({ + findById: (id: ChannelId) => + Effect.succeed( + Option.some({ + id, + organizationId, + }), + ), + }), + ) const makeServiceLayer = (botIds: ReadonlyArray) => Layer.effect(BotGatewayService, BotGatewayService.make).pipe( diff --git a/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.test.ts b/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.test.ts index ce9d47049..ddb1d899b 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.test.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-attribution-reconciler.test.ts @@ -17,9 +17,7 @@ const makeLayer = (deps: { Layer.effect(ChatSyncAttributionReconciler, ChatSyncAttributionReconciler.make).pipe( Layer.provide(Layer.succeed(MessageRepo, deps.messageRepo)), Layer.provide(Layer.succeed(UserRepo, deps.userRepo)), - Layer.provide( - Layer.succeed(OrganizationMemberRepo, deps.organizationMemberRepo), - ), + Layer.provide(Layer.succeed(OrganizationMemberRepo, deps.organizationMemberRepo)), ) describe("ChatSyncAttributionReconciler", () => { diff --git a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts index 89bbe8f9b..2359ac069 100644 --- a/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts +++ b/apps/backend/src/services/chat-sync/chat-sync-core-worker.ts @@ -162,663 +162,672 @@ export class ChatSyncCoreWorker extends ServiceMap.Service< /** @internal — exported for integration tests that provide their own deps */ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export const ChatSyncCoreWorkerMake: Effect.Effect, unknown, unknown> = - Effect.gen(function* () { - const db = yield* Database.Database - const connectionRepo = yield* ChatSyncConnectionRepo - const channelLinkRepo = yield* ChatSyncChannelLinkRepo - const messageLinkRepo = yield* ChatSyncMessageLinkRepo - const eventReceiptRepo = yield* ChatSyncEventReceiptRepo - const messageRepo = yield* MessageRepo - const outboxRepo = yield* MessageOutboxRepo - const messageReactionRepo = yield* MessageReactionRepo - const channelRepo = yield* ChannelRepo - const integrationConnectionRepo = yield* IntegrationConnectionRepo - const userRepo = yield* UserRepo - const organizationMemberRepo = yield* OrganizationMemberRepo - const integrationBotService = yield* IntegrationBotService - const channelAccessSyncService = yield* ChannelAccessSyncService - const providerRegistry = yield* ChatSyncProviderRegistry - const discordApiClient = yield* Discord.DiscordApiClient - - const payloadHash = (value: unknown): string => - createHash("sha256").update(JSON.stringify(value)).digest("hex") - - const claimReceipt = Effect.fn("discordSyncWorker.claimReceipt")(function* (params: { - syncConnectionId: SyncConnectionId - channelLinkId?: SyncChannelLinkId - source: "hazel" | "external" - dedupeKey: string - }) { - return yield* eventReceiptRepo.claimByDedupeKey({ - syncConnectionId: params.syncConnectionId, - channelLinkId: params.channelLinkId, - source: params.source, - dedupeKey: params.dedupeKey, +export const ChatSyncCoreWorkerMake: Effect.Effect, unknown, unknown> = Effect.gen( + function* () { + const db = yield* Database.Database + const connectionRepo = yield* ChatSyncConnectionRepo + const channelLinkRepo = yield* ChatSyncChannelLinkRepo + const messageLinkRepo = yield* ChatSyncMessageLinkRepo + const eventReceiptRepo = yield* ChatSyncEventReceiptRepo + const messageRepo = yield* MessageRepo + const outboxRepo = yield* MessageOutboxRepo + const messageReactionRepo = yield* MessageReactionRepo + const channelRepo = yield* ChannelRepo + const integrationConnectionRepo = yield* IntegrationConnectionRepo + const userRepo = yield* UserRepo + const organizationMemberRepo = yield* OrganizationMemberRepo + const integrationBotService = yield* IntegrationBotService + const channelAccessSyncService = yield* ChannelAccessSyncService + const providerRegistry = yield* ChatSyncProviderRegistry + const discordApiClient = yield* Discord.DiscordApiClient + + const payloadHash = (value: unknown): string => + createHash("sha256").update(JSON.stringify(value)).digest("hex") + + const claimReceipt = Effect.fn("discordSyncWorker.claimReceipt")(function* (params: { + syncConnectionId: SyncConnectionId + channelLinkId?: SyncChannelLinkId + source: "hazel" | "external" + dedupeKey: string + }) { + return yield* eventReceiptRepo.claimByDedupeKey({ + syncConnectionId: params.syncConnectionId, + channelLinkId: params.channelLinkId, + source: params.source, + dedupeKey: params.dedupeKey, + }) }) - }) - - const writeReceipt = Effect.fn("discordSyncWorker.writeReceipt")(function* (params: { - syncConnectionId: SyncConnectionId - channelLinkId?: SyncChannelLinkId - source: "hazel" | "external" - dedupeKey: string - status?: "processed" | "ignored" | "failed" - errorMessage?: string - payload?: unknown - }) { - yield* eventReceiptRepo.updateByDedupeKey({ - syncConnectionId: params.syncConnectionId, - source: params.source, - dedupeKey: params.dedupeKey, - channelLinkId: params.channelLinkId, - externalEventId: null, - payloadHash: params.payload ? payloadHash(params.payload) : null, - status: params.status ?? "processed", - errorMessage: params.errorMessage ?? null, + + const writeReceipt = Effect.fn("discordSyncWorker.writeReceipt")(function* (params: { + syncConnectionId: SyncConnectionId + channelLinkId?: SyncChannelLinkId + source: "hazel" | "external" + dedupeKey: string + status?: "processed" | "ignored" | "failed" + errorMessage?: string + payload?: unknown + }) { + yield* eventReceiptRepo.updateByDedupeKey({ + syncConnectionId: params.syncConnectionId, + source: params.source, + dedupeKey: params.dedupeKey, + channelLinkId: params.channelLinkId, + externalEventId: null, + payloadHash: params.payload ? payloadHash(params.payload) : null, + status: params.status ?? "processed", + errorMessage: params.errorMessage ?? null, + }) + }) + + const getProviderAdapter = Effect.fn("ChatSyncCoreWorker.getProviderAdapter")(function* ( + provider: ChatSyncProvider, + ) { + return yield* providerRegistry.getAdapter(provider) }) - }) - - const getProviderAdapter = Effect.fn("ChatSyncCoreWorker.getProviderAdapter")(function* ( - provider: ChatSyncProvider, - ) { - return yield* providerRegistry.getAdapter(provider) - }) - - const buildAttachmentPublicUrl = (baseUrl: string, attachmentId: string): string => { - const normalizedBase = baseUrl.replace(/\/+$/, "") - return `${normalizedBase}/${attachmentId}` - } - - const getAttachmentPublicUrlBase = Effect.fn("discordSyncWorker.getAttachmentPublicUrlBase")( - function* () { - const configuredBaseUrl = yield* Config.string("S3_PUBLIC_URL").pipe(Config.option) - if (Option.isNone(configuredBaseUrl) || configuredBaseUrl.value.trim().length === 0) { - return yield* Effect.fail( - new DiscordSyncConfigurationError({ - message: "S3_PUBLIC_URL is required for syncing message attachments", - }), - ) - } - return configuredBaseUrl.value.trim() - }, - ) - - const listMessageAttachmentsForOutboundSync = Effect.fn( - "discordSyncWorker.listMessageAttachmentsForOutboundSync", - )(function* (hazelMessageId: MessageId) { - const rows = yield* db.execute((client) => - client - .select({ - id: schema.attachmentsTable.id, - fileName: schema.attachmentsTable.fileName, - fileSize: schema.attachmentsTable.fileSize, - }) - .from(schema.attachmentsTable) - .where( - and( - eq(schema.attachmentsTable.messageId, hazelMessageId), - eq(schema.attachmentsTable.status, "complete"), - isNull(schema.attachmentsTable.deletedAt), - ), - ) - .orderBy(asc(schema.attachmentsTable.uploadedAt), asc(schema.attachmentsTable.id)), - ) - if (rows.length === 0) { - return [] + const buildAttachmentPublicUrl = (baseUrl: string, attachmentId: string): string => { + const normalizedBase = baseUrl.replace(/\/+$/, "") + return `${normalizedBase}/${attachmentId}` } - const publicUrlBase = yield* getAttachmentPublicUrlBase() - return rows.map((row) => ({ - id: row.id, - fileName: row.fileName, - fileSize: row.fileSize, - publicUrl: buildAttachmentPublicUrl(publicUrlBase, row.id), - })) - }) - - const getOrCreateShadowUserId = Effect.fn("discordSyncWorker.getOrCreateShadowUserId")( - function* (params: { - provider: ChatSyncProvider - organizationId: OrganizationId - externalUserId: ExternalUserId - displayName: string - avatarUrl: string | null - syncAvatarUrl?: boolean - }) { - const externalId = `${params.provider}-user-${params.externalUserId}` - const user = yield* userRepo.upsertByExternalId( - { - externalId, - email: `${externalId}@${params.provider}.internal`, - firstName: params.displayName, - lastName: "", - avatarUrl: params.avatarUrl ?? "", - userType: "machine", - settings: null, - isOnboarded: true, - timezone: null, - deletedAt: null, - }, - { syncAvatarUrl: params.syncAvatarUrl ?? false }, + const getAttachmentPublicUrlBase = Effect.fn("discordSyncWorker.getAttachmentPublicUrlBase")( + function* () { + const configuredBaseUrl = yield* Config.string("S3_PUBLIC_URL").pipe(Config.option) + if (Option.isNone(configuredBaseUrl) || configuredBaseUrl.value.trim().length === 0) { + return yield* Effect.fail( + new DiscordSyncConfigurationError({ + message: "S3_PUBLIC_URL is required for syncing message attachments", + }), + ) + } + return configuredBaseUrl.value.trim() + }, + ) + + const listMessageAttachmentsForOutboundSync = Effect.fn( + "discordSyncWorker.listMessageAttachmentsForOutboundSync", + )(function* (hazelMessageId: MessageId) { + const rows = yield* db.execute((client) => + client + .select({ + id: schema.attachmentsTable.id, + fileName: schema.attachmentsTable.fileName, + fileSize: schema.attachmentsTable.fileSize, + }) + .from(schema.attachmentsTable) + .where( + and( + eq(schema.attachmentsTable.messageId, hazelMessageId), + eq(schema.attachmentsTable.status, "complete"), + isNull(schema.attachmentsTable.deletedAt), + ), + ) + .orderBy(asc(schema.attachmentsTable.uploadedAt), asc(schema.attachmentsTable.id)), ) - yield* organizationMemberRepo.upsertByOrgAndUser({ - organizationId: params.organizationId, - userId: user.id, - role: "member", - nickname: null, - joinedAt: new Date(), - invitedBy: null, - deletedAt: null, - }) + if (rows.length === 0) { + return [] + } - return user.id - }, - ) + const publicUrlBase = yield* getAttachmentPublicUrlBase() + return rows.map((row) => ({ + id: row.id, + fileName: row.fileName, + fileSize: row.fileSize, + publicUrl: buildAttachmentPublicUrl(publicUrlBase, row.id), + })) + }) + + const getOrCreateShadowUserId = Effect.fn("discordSyncWorker.getOrCreateShadowUserId")( + function* (params: { + provider: ChatSyncProvider + organizationId: OrganizationId + externalUserId: ExternalUserId + displayName: string + avatarUrl: string | null + syncAvatarUrl?: boolean + }) { + const externalId = `${params.provider}-user-${params.externalUserId}` + const user = yield* userRepo.upsertByExternalId( + { + externalId, + email: `${externalId}@${params.provider}.internal`, + firstName: params.displayName, + lastName: "", + avatarUrl: params.avatarUrl ?? "", + userType: "machine", + settings: null, + isOnboarded: true, + timezone: null, + deletedAt: null, + }, + { syncAvatarUrl: params.syncAvatarUrl ?? false }, + ) - const decodeProvider = Schema.decodeUnknownSync(IntegrationConnection.IntegrationProvider) + yield* organizationMemberRepo.upsertByOrgAndUser({ + organizationId: params.organizationId, + userId: user.id, + role: "member", + nickname: null, + joinedAt: new Date(), + invitedBy: null, + deletedAt: null, + }) - type WebhookPermissionStatus = "unknown" | "allowed" | "denied" + return user.id + }, + ) - type WebhookPermissionState = { - status: WebhookPermissionStatus - checkedAt: string - reason?: string - } + const decodeProvider = Schema.decodeUnknownSync(IntegrationConnection.IntegrationProvider) - const getRawWebhookPermissionState = ( - settings: Record | null | undefined, - ): WebhookPermissionState | undefined => { - const raw = settings?.webhookPermission - if (!raw || typeof raw !== "object") { - return undefined - } + type WebhookPermissionStatus = "unknown" | "allowed" | "denied" - const status = (raw as { status?: unknown }).status - if (status !== "allowed" && status !== "denied" && status !== "unknown") { - return undefined + type WebhookPermissionState = { + status: WebhookPermissionStatus + checkedAt: string + reason?: string } - const checkedAt = (raw as { checkedAt?: unknown }).checkedAt - const reason = (raw as { reason?: unknown }).reason + const getRawWebhookPermissionState = ( + settings: Record | null | undefined, + ): WebhookPermissionState | undefined => { + const raw = settings?.webhookPermission + if (!raw || typeof raw !== "object") { + return undefined + } - return { - status, - checkedAt: - typeof checkedAt === "string" && checkedAt.trim().length > 0 - ? checkedAt - : new Date().toISOString(), - reason: typeof reason === "string" && reason.trim().length > 0 ? reason : undefined, - } - } - - const makeWebhookPermissionState = (params: { - status: WebhookPermissionStatus - reason?: string - }): WebhookPermissionState => ({ - status: params.status, - checkedAt: new Date().toISOString(), - ...(params.reason ? { reason: params.reason } : {}), - }) - - const isDiscordApiError = (error: unknown): error is Discord.DiscordApiError => - typeof error === "object" && - error !== null && - (error as { _tag?: unknown })._tag === "DiscordApiError" - - const isWebhookStrategyEnabled = ( - outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, - ): boolean => outboundIdentity.enabled && outboundIdentity.strategy === "webhook" - - const defaultOutboundIdentitySettings = (): ChatSyncChannelLink.OutboundIdentitySettings => ({ - enabled: false, - strategy: "webhook", - providers: {}, - }) - - const getRawOutboundIdentitySettings = ( - settings: Record | null | undefined, - ): Record | undefined => { - const raw = settings?.outboundIdentity - if (!raw || typeof raw !== "object") { - return undefined - } - return raw as Record - } - - const getOutboundIdentitySettings = ( - settings: Record | null | undefined, - ): ChatSyncChannelLink.OutboundIdentitySettings => { - const raw = getRawOutboundIdentitySettings(settings) - if (raw === undefined) { - return defaultOutboundIdentitySettings() - } - try { - return Schema.decodeUnknownSync(ChatSyncChannelLink.OutboundIdentitySettings)(raw) - } catch { - return defaultOutboundIdentitySettings() - } - } - - const getDiscordWebhookConfig = ( - outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, - ): Option.Option => { - const providerConfig = (outboundIdentity.providers as Record)["discord"] - if ( - typeof providerConfig !== "object" || - providerConfig === null || - !(providerConfig as { kind?: unknown }).kind - ) { - return Option.none() - } + const status = (raw as { status?: unknown }).status + if (status !== "allowed" && status !== "denied" && status !== "unknown") { + return undefined + } - if ((providerConfig as { kind?: string }).kind !== "discord.webhook") { - return Option.none() - } + const checkedAt = (raw as { checkedAt?: unknown }).checkedAt + const reason = (raw as { reason?: unknown }).reason - try { - return Option.some( - Schema.decodeUnknownSync(ChatSyncChannelLink.DiscordWebhookOutboundIdentityConfig)( - providerConfig, - ), - ) - } catch { - return Option.none() - } - } - - const shouldIgnoreWebhookOrigin = ( - provider: ChatSyncProvider, - settings: Record | null | undefined, - externalWebhookId: ExternalWebhookId | undefined, - ): boolean => { - if (!externalWebhookId || provider !== "discord") { - return false - } - const outboundIdentity = getOutboundIdentitySettings(settings) - if (!isWebhookStrategyEnabled(outboundIdentity)) { - return false + return { + status, + checkedAt: + typeof checkedAt === "string" && checkedAt.trim().length > 0 + ? checkedAt + : new Date().toISOString(), + reason: typeof reason === "string" && reason.trim().length > 0 ? reason : undefined, + } } - const webhookConfig = getDiscordWebhookConfig(outboundIdentity) - return Option.isSome(webhookConfig) && webhookConfig.value.webhookId === externalWebhookId - } - - const persistWebhookIdentity = Effect.fn("discordSyncWorker.persistWebhookIdentity")(function* ( - link: { - id: SyncChannelLinkId - settings: Record | null - }, - outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, - webhookPermissionState?: WebhookPermissionState, - ) { - const currentRawOutboundIdentity = - getRawOutboundIdentitySettings(link.settings) ?? defaultOutboundIdentitySettings() - const currentProviders = - typeof currentRawOutboundIdentity.providers === "object" && - currentRawOutboundIdentity.providers !== null - ? (currentRawOutboundIdentity.providers as Record) - : {} - - const nextSettings = { - ...(link.settings ?? {}), - outboundIdentity: { - ...currentRawOutboundIdentity, - enabled: outboundIdentity.enabled, - strategy: outboundIdentity.strategy, - providers: { - ...currentProviders, - ...outboundIdentity.providers, - }, - }, - ...(webhookPermissionState ? { webhookPermission: webhookPermissionState } : {}), + + const makeWebhookPermissionState = (params: { + status: WebhookPermissionStatus + reason?: string + }): WebhookPermissionState => ({ + status: params.status, + checkedAt: new Date().toISOString(), + ...(params.reason ? { reason: params.reason } : {}), + }) + + const isDiscordApiError = (error: unknown): error is Discord.DiscordApiError => + typeof error === "object" && + error !== null && + (error as { _tag?: unknown })._tag === "DiscordApiError" + + const isWebhookStrategyEnabled = ( + outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, + ): boolean => outboundIdentity.enabled && outboundIdentity.strategy === "webhook" + + const defaultOutboundIdentitySettings = (): ChatSyncChannelLink.OutboundIdentitySettings => ({ + enabled: false, + strategy: "webhook", + providers: {}, + }) + + const getRawOutboundIdentitySettings = ( + settings: Record | null | undefined, + ): Record | undefined => { + const raw = settings?.outboundIdentity + if (!raw || typeof raw !== "object") { + return undefined + } + return raw as Record } - yield* channelLinkRepo.updateSettings(link.id, nextSettings) - }) - const ensureDiscordWebhookIdentity = Effect.fn("discordSyncWorker.ensureDiscordWebhookIdentity")( - function* (params: { - provider: ChatSyncProvider - link: { - id: SyncChannelLinkId - externalChannelId: ExternalChannelId - settings: Record | null + const getOutboundIdentitySettings = ( + settings: Record | null | undefined, + ): ChatSyncChannelLink.OutboundIdentitySettings => { + const raw = getRawOutboundIdentitySettings(settings) + if (raw === undefined) { + return defaultOutboundIdentitySettings() } - }) { - const outboundIdentity = getOutboundIdentitySettings(params.link.settings) - if (params.provider !== "discord") { - return Option.none() + try { + return Schema.decodeUnknownSync(ChatSyncChannelLink.OutboundIdentitySettings)(raw) + } catch { + return defaultOutboundIdentitySettings() } + } - if (!isWebhookStrategyEnabled(outboundIdentity)) { + const getDiscordWebhookConfig = ( + outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, + ): Option.Option => { + const providerConfig = (outboundIdentity.providers as Record)["discord"] + if ( + typeof providerConfig !== "object" || + providerConfig === null || + !(providerConfig as { kind?: unknown }).kind + ) { return Option.none() } - const webhookPermissionState = - getRawWebhookPermissionState(params.link.settings) ?? - makeWebhookPermissionState({ status: "unknown" }) - if (webhookPermissionState.status === "denied") { + if ((providerConfig as { kind?: string }).kind !== "discord.webhook") { return Option.none() } - return yield* Effect.gen(function* () { - const currentConfig = getDiscordWebhookConfig(outboundIdentity) - if (Option.isSome(currentConfig)) { - if (!outboundIdentity.enabled) { - return Option.none() - } - return currentConfig - } - - const botTokenOption = yield* Config.redacted("DISCORD_BOT_TOKEN").pipe(Config.option) - if (Option.isNone(botTokenOption)) { - return Option.none() - } - const botToken = Redacted.value(botTokenOption.value) - - const created = yield* discordApiClient.createWebhook({ - channelId: params.link.externalChannelId, - botToken, - }) - - const nextConfig: ChatSyncChannelLink.DiscordWebhookOutboundIdentityConfig = { - kind: "discord.webhook", - webhookId: created.webhookId, - webhookToken: created.webhookToken, - } - - const nextOutboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings = { - enabled: true, - strategy: "webhook", - providers: { - ...outboundIdentity.providers, - discord: nextConfig, - }, - } - - yield* persistWebhookIdentity( - params.link, - nextOutboundIdentity, - makeWebhookPermissionState({ status: "allowed" }), + try { + return Option.some( + Schema.decodeUnknownSync(ChatSyncChannelLink.DiscordWebhookOutboundIdentityConfig)( + providerConfig, + ), ) - return Option.some(nextConfig) - }).pipe( - Effect.catch((error) => - Effect.gen(function* () { - if (isDiscordApiError(error) && error.status === 403) { - const fallbackOutboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings = { - enabled: outboundIdentity.enabled, - strategy: "fallback_bot", - providers: outboundIdentity.providers, - } - - yield* persistWebhookIdentity( - params.link, - fallbackOutboundIdentity, - makeWebhookPermissionState({ - status: "denied", - reason: `Webhook create forbidden (HTTP ${error.status})`, - }), - ) - } else { - const reason = - typeof error === "object" && error !== null && "message" in error - ? String((error as { message?: unknown }).message) - : undefined - - yield* persistWebhookIdentity( - params.link, - outboundIdentity, - makeWebhookPermissionState({ status: "unknown", reason }), - ) - } - - yield* Effect.logWarning("Failed to provision Discord webhook identity", { - provider: params.provider, - externalChannelId: params.link.externalChannelId, - error: String(error), - }) - return Option.none() - }), - ), - ) - }, - ) - - const getDiscordWebhookIdentityMessageMetadata = Effect.fn( - "discordSyncWorker.getDiscordWebhookIdentityMessageMetadata", - )(function* (authorId: UserId) { - const userOption = yield* userRepo.findById(authorId) - if (Option.isNone(userOption)) { - return { - username: "Discord User", - avatarUrl: undefined as string | undefined, + } catch { + return Option.none() } } - const user = userOption.value - const username = [user.firstName, user.lastName].filter(Boolean).join(" ").trim() || user.firstName - const avatarUrl = user.avatarUrl && user.avatarUrl.trim() ? user.avatarUrl : undefined - return { username, avatarUrl } - }) + const shouldIgnoreWebhookOrigin = ( + provider: ChatSyncProvider, + settings: Record | null | undefined, + externalWebhookId: ExternalWebhookId | undefined, + ): boolean => { + if (!externalWebhookId || provider !== "discord") { + return false + } + const outboundIdentity = getOutboundIdentitySettings(settings) + if (!isWebhookStrategyEnabled(outboundIdentity)) { + return false + } + const webhookConfig = getDiscordWebhookConfig(outboundIdentity) + return Option.isSome(webhookConfig) && webhookConfig.value.webhookId === externalWebhookId + } - const sendDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.sendDiscordMessageViaWebhook")( - function* (params: { + const persistWebhookIdentity = Effect.fn("discordSyncWorker.persistWebhookIdentity")(function* ( link: { id: SyncChannelLinkId - externalChannelId: ExternalChannelId settings: Record | null + }, + outboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings, + webhookPermissionState?: WebhookPermissionState, + ) { + const currentRawOutboundIdentity = + getRawOutboundIdentitySettings(link.settings) ?? defaultOutboundIdentitySettings() + const currentProviders = + typeof currentRawOutboundIdentity.providers === "object" && + currentRawOutboundIdentity.providers !== null + ? (currentRawOutboundIdentity.providers as Record) + : {} + + const nextSettings = { + ...(link.settings ?? {}), + outboundIdentity: { + ...currentRawOutboundIdentity, + enabled: outboundIdentity.enabled, + strategy: outboundIdentity.strategy, + providers: { + ...currentProviders, + ...outboundIdentity.providers, + }, + }, + ...(webhookPermissionState ? { webhookPermission: webhookPermissionState } : {}), } - message: { authorId: UserId } - content: string - attachments?: ReadonlyArray - replyToExternalMessageId?: ExternalMessageId - }) { - return yield* Effect.gen(function* () { + yield* channelLinkRepo.updateSettings(link.id, nextSettings) + }) + + const ensureDiscordWebhookIdentity = Effect.fn("discordSyncWorker.ensureDiscordWebhookIdentity")( + function* (params: { + provider: ChatSyncProvider + link: { + id: SyncChannelLinkId + externalChannelId: ExternalChannelId + settings: Record | null + } + }) { const outboundIdentity = getOutboundIdentitySettings(params.link.settings) + if (params.provider !== "discord") { + return Option.none() + } + if (!isWebhookStrategyEnabled(outboundIdentity)) { return Option.none() } - const config = yield* ensureDiscordWebhookIdentity({ - provider: "discord", - link: params.link, - }) - if (Option.isNone(config)) { + const webhookPermissionState = + getRawWebhookPermissionState(params.link.settings) ?? + makeWebhookPermissionState({ status: "unknown" }) + if (webhookPermissionState.status === "denied") { return Option.none() } - const metadata = yield* getDiscordWebhookIdentityMessageMetadata(params.message.authorId) - const outboundContent = - params.attachments && params.attachments.length > 0 - ? formatMessageContentWithAttachments({ - content: params.content, - attachments: params.attachments, - }) - : params.content - const outboundMessageId = yield* discordApiClient.executeWebhookMessage({ - webhookId: config.value.webhookId, - webhookToken: config.value.webhookToken, - content: outboundContent, - replyToExternalMessageId: params.replyToExternalMessageId, - username: metadata.username, - avatarUrl: metadata.avatarUrl ?? config.value.defaultAvatarUrl, - }) + return yield* Effect.gen(function* () { + const currentConfig = getDiscordWebhookConfig(outboundIdentity) + if (Option.isSome(currentConfig)) { + if (!outboundIdentity.enabled) { + return Option.none() + } + return currentConfig + } - return Option.some(outboundMessageId as ExternalMessageId) - }).pipe( - Effect.catch((error) => - Effect.gen(function* () { - yield* Effect.logWarning("Discord webhook send failed; falling back to bot API", { - error: String(error), - }) + const botTokenOption = yield* Config.redacted("DISCORD_BOT_TOKEN").pipe(Config.option) + if (Option.isNone(botTokenOption)) { return Option.none() - }), - ), - ) - }, - ) + } + const botToken = Redacted.value(botTokenOption.value) - const updateDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.updateDiscordMessageViaWebhook")( - function* (params: { - link: { - id: SyncChannelLinkId - externalChannelId: ExternalChannelId - settings: Record | null - } - externalMessageId: ExternalMessageId - content: string - }) { - return yield* Effect.gen(function* () { - const outboundIdentity = getOutboundIdentitySettings(params.link.settings) - if (!isWebhookStrategyEnabled(outboundIdentity)) { - return false - } + const created = yield* discordApiClient.createWebhook({ + channelId: params.link.externalChannelId, + botToken, + }) - const config = yield* ensureDiscordWebhookIdentity({ - provider: "discord", - link: params.link, - }) - if (Option.isNone(config)) { - return false - } + const nextConfig: ChatSyncChannelLink.DiscordWebhookOutboundIdentityConfig = { + kind: "discord.webhook", + webhookId: created.webhookId, + webhookToken: created.webhookToken, + } - yield* discordApiClient.updateWebhookMessage({ - webhookId: config.value.webhookId, - webhookToken: config.value.webhookToken, - webhookMessageId: params.externalMessageId, - content: params.content, - }) + const nextOutboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings = { + enabled: true, + strategy: "webhook", + providers: { + ...outboundIdentity.providers, + discord: nextConfig, + }, + } - return true - }).pipe( - Effect.catch((error) => - Effect.gen(function* () { - yield* Effect.logWarning("Discord webhook update failed; falling back to bot API", { - error: String(error), - }) - return false - }), - ), - ) - }, - ) + yield* persistWebhookIdentity( + params.link, + nextOutboundIdentity, + makeWebhookPermissionState({ status: "allowed" }), + ) + return Option.some(nextConfig) + }).pipe( + Effect.catch((error) => + Effect.gen(function* () { + if (isDiscordApiError(error) && error.status === 403) { + const fallbackOutboundIdentity: ChatSyncChannelLink.OutboundIdentitySettings = + { + enabled: outboundIdentity.enabled, + strategy: "fallback_bot", + providers: outboundIdentity.providers, + } + + yield* persistWebhookIdentity( + params.link, + fallbackOutboundIdentity, + makeWebhookPermissionState({ + status: "denied", + reason: `Webhook create forbidden (HTTP ${error.status})`, + }), + ) + } else { + const reason = + typeof error === "object" && error !== null && "message" in error + ? String((error as { message?: unknown }).message) + : undefined + + yield* persistWebhookIdentity( + params.link, + outboundIdentity, + makeWebhookPermissionState({ status: "unknown", reason }), + ) + } - const deleteDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.deleteDiscordMessageViaWebhook")( - function* (params: { - link: { - id: SyncChannelLinkId - externalChannelId: ExternalChannelId - settings: Record | null - } - externalMessageId: ExternalMessageId - }) { - return yield* Effect.gen(function* () { - const outboundIdentity = getOutboundIdentitySettings(params.link.settings) - if (!isWebhookStrategyEnabled(outboundIdentity)) { - return false + yield* Effect.logWarning("Failed to provision Discord webhook identity", { + provider: params.provider, + externalChannelId: params.link.externalChannelId, + error: String(error), + }) + return Option.none() + }), + ), + ) + }, + ) + + const getDiscordWebhookIdentityMessageMetadata = Effect.fn( + "discordSyncWorker.getDiscordWebhookIdentityMessageMetadata", + )(function* (authorId: UserId) { + const userOption = yield* userRepo.findById(authorId) + if (Option.isNone(userOption)) { + return { + username: "Discord User", + avatarUrl: undefined as string | undefined, } + } - const config = yield* ensureDiscordWebhookIdentity({ - provider: "discord", - link: params.link, - }) - if (Option.isNone(config)) { - return false + const user = userOption.value + const username = + [user.firstName, user.lastName].filter(Boolean).join(" ").trim() || user.firstName + const avatarUrl = user.avatarUrl && user.avatarUrl.trim() ? user.avatarUrl : undefined + return { username, avatarUrl } + }) + + const sendDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.sendDiscordMessageViaWebhook")( + function* (params: { + link: { + id: SyncChannelLinkId + externalChannelId: ExternalChannelId + settings: Record | null } + message: { authorId: UserId } + content: string + attachments?: ReadonlyArray + replyToExternalMessageId?: ExternalMessageId + }) { + return yield* Effect.gen(function* () { + const outboundIdentity = getOutboundIdentitySettings(params.link.settings) + if (!isWebhookStrategyEnabled(outboundIdentity)) { + return Option.none() + } - yield* discordApiClient.deleteWebhookMessage({ - webhookId: config.value.webhookId, - webhookToken: config.value.webhookToken, - webhookMessageId: params.externalMessageId, - }) + const config = yield* ensureDiscordWebhookIdentity({ + provider: "discord", + link: params.link, + }) + if (Option.isNone(config)) { + return Option.none() + } - return true - }).pipe( - Effect.catch((error) => - Effect.gen(function* () { - yield* Effect.logWarning("Discord webhook delete failed; falling back to bot API", { - error: String(error), - }) - return false - }), - ), - ) - }, - ) - - const resolveAuthorUserId = Effect.fn("discordSyncWorker.resolveAuthorUserId")(function* (params: { - provider: ChatSyncProvider - organizationId: OrganizationId - externalUserId: ExternalUserId - displayName: string - avatarUrl: string | null - syncAvatarUrl?: boolean - }) { - const linkedConnection = yield* integrationConnectionRepo.findActiveUserByExternalAccountId( - params.organizationId, - decodeProvider(params.provider), - params.externalUserId, + const metadata = yield* getDiscordWebhookIdentityMessageMetadata(params.message.authorId) + const outboundContent = + params.attachments && params.attachments.length > 0 + ? formatMessageContentWithAttachments({ + content: params.content, + attachments: params.attachments, + }) + : params.content + const outboundMessageId = yield* discordApiClient.executeWebhookMessage({ + webhookId: config.value.webhookId, + webhookToken: config.value.webhookToken, + content: outboundContent, + replyToExternalMessageId: params.replyToExternalMessageId, + username: metadata.username, + avatarUrl: metadata.avatarUrl ?? config.value.defaultAvatarUrl, + }) + + return Option.some(outboundMessageId as ExternalMessageId) + }).pipe( + Effect.catch((error) => + Effect.gen(function* () { + yield* Effect.logWarning("Discord webhook send failed; falling back to bot API", { + error: String(error), + }) + return Option.none() + }), + ), + ) + }, ) - if (Option.isSome(linkedConnection) && linkedConnection.value.userId) { - return linkedConnection.value.userId - } + const updateDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.updateDiscordMessageViaWebhook")( + function* (params: { + link: { + id: SyncChannelLinkId + externalChannelId: ExternalChannelId + settings: Record | null + } + externalMessageId: ExternalMessageId + content: string + }) { + return yield* Effect.gen(function* () { + const outboundIdentity = getOutboundIdentitySettings(params.link.settings) + if (!isWebhookStrategyEnabled(outboundIdentity)) { + return false + } - const shouldSyncAvatarUrl = - params.syncAvatarUrl !== undefined - ? params.syncAvatarUrl - : !!(params.avatarUrl && params.avatarUrl.trim()) - - return yield* getOrCreateShadowUserId({ - provider: params.provider, - organizationId: params.organizationId, - externalUserId: params.externalUserId, - displayName: params.displayName, - avatarUrl: params.avatarUrl, - syncAvatarUrl: shouldSyncAvatarUrl, - }) - }) + const config = yield* ensureDiscordWebhookIdentity({ + provider: "discord", + link: params.link, + }) + if (Option.isNone(config)) { + return false + } - const DISCORD_USER_MENTION_PATTERN = /@\[userId:([^\]]+)\]/g + yield* discordApiClient.updateWebhookMessage({ + webhookId: config.value.webhookId, + webhookToken: config.value.webhookToken, + webhookMessageId: params.externalMessageId, + content: params.content, + }) - const getDiscordMentionFallbackDisplayName = Effect.fn( - "discordSyncWorker.getDiscordMentionFallbackDisplayName", - )(function* (userId: UserId) { - const userOption = yield* userRepo.findById(userId) - if (Option.isNone(userOption)) { - return "@unknown-user" - } + return true + }).pipe( + Effect.catch((error) => + Effect.gen(function* () { + yield* Effect.logWarning( + "Discord webhook update failed; falling back to bot API", + { + error: String(error), + }, + ) + return false + }), + ), + ) + }, + ) - const user = userOption.value - const fullName = [user.firstName, user.lastName].filter(Boolean).join(" ").trim() - if (fullName.length > 0) { - return `@${fullName}` - } + const deleteDiscordMessageViaWebhook = Effect.fn("discordSyncWorker.deleteDiscordMessageViaWebhook")( + function* (params: { + link: { + id: SyncChannelLinkId + externalChannelId: ExternalChannelId + settings: Record | null + } + externalMessageId: ExternalMessageId + }) { + return yield* Effect.gen(function* () { + const outboundIdentity = getOutboundIdentitySettings(params.link.settings) + if (!isWebhookStrategyEnabled(outboundIdentity)) { + return false + } - const firstName = user.firstName.trim() - if (firstName.length > 0) { - return `@${firstName}` - } + const config = yield* ensureDiscordWebhookIdentity({ + provider: "discord", + link: params.link, + }) + if (Option.isNone(config)) { + return false + } - return "@unknown-user" - }) + yield* discordApiClient.deleteWebhookMessage({ + webhookId: config.value.webhookId, + webhookToken: config.value.webhookToken, + webhookMessageId: params.externalMessageId, + }) - const translateHazelMentionsForDiscord = Effect.fn("discordSyncWorker.translateHazelMentionsForDiscord")( - function* (params: { organizationId: OrganizationId; content: string }) { - const userIds = [...params.content.matchAll(DISCORD_USER_MENTION_PATTERN)].map( - (match) => match[1] as UserId, + return true + }).pipe( + Effect.catch((error) => + Effect.gen(function* () { + yield* Effect.logWarning( + "Discord webhook delete failed; falling back to bot API", + { + error: String(error), + }, + ) + return false + }), + ), + ) + }, + ) + + const resolveAuthorUserId = Effect.fn("discordSyncWorker.resolveAuthorUserId")(function* (params: { + provider: ChatSyncProvider + organizationId: OrganizationId + externalUserId: ExternalUserId + displayName: string + avatarUrl: string | null + syncAvatarUrl?: boolean + }) { + const linkedConnection = yield* integrationConnectionRepo.findActiveUserByExternalAccountId( + params.organizationId, + decodeProvider(params.provider), + params.externalUserId, ) - if (userIds.length === 0) { - return params.content + if (Option.isSome(linkedConnection) && linkedConnection.value.userId) { + return linkedConnection.value.userId } - const replacements = new Map() + const shouldSyncAvatarUrl = + params.syncAvatarUrl !== undefined + ? params.syncAvatarUrl + : !!(params.avatarUrl && params.avatarUrl.trim()) - for (const userId of new Set(userIds)) { - const connectionOption = yield* integrationConnectionRepo.findUserConnection( + return yield* getOrCreateShadowUserId({ + provider: params.provider, + organizationId: params.organizationId, + externalUserId: params.externalUserId, + displayName: params.displayName, + avatarUrl: params.avatarUrl, + syncAvatarUrl: shouldSyncAvatarUrl, + }) + }) + + const DISCORD_USER_MENTION_PATTERN = /@\[userId:([^\]]+)\]/g + + const getDiscordMentionFallbackDisplayName = Effect.fn( + "discordSyncWorker.getDiscordMentionFallbackDisplayName", + )(function* (userId: UserId) { + const userOption = yield* userRepo.findById(userId) + if (Option.isNone(userOption)) { + return "@unknown-user" + } + + const user = userOption.value + const fullName = [user.firstName, user.lastName].filter(Boolean).join(" ").trim() + if (fullName.length > 0) { + return `@${fullName}` + } + + const firstName = user.firstName.trim() + if (firstName.length > 0) { + return `@${firstName}` + } + + return "@unknown-user" + }) + + const translateHazelMentionsForDiscord = Effect.fn( + "discordSyncWorker.translateHazelMentionsForDiscord", + )(function* (params: { organizationId: OrganizationId; content: string }) { + const userIds = [...params.content.matchAll(DISCORD_USER_MENTION_PATTERN)].map( + (match) => match[1] as UserId, + ) + + if (userIds.length === 0) { + return params.content + } + + const replacements = new Map() + + for (const userId of new Set(userIds)) { + const connectionOption = yield* integrationConnectionRepo.findUserConnection( params.organizationId, userId, "discord", @@ -842,504 +851,522 @@ export const ChatSyncCoreWorkerMake: Effect.Effect, unk DISCORD_USER_MENTION_PATTERN, (fullMatch, userId: string) => replacements.get(userId) ?? fullMatch, ) - }, - ) + }) - const normalizeChannelLinkExternalId = (link: T) => ({ - ...link, - externalChannelId: link.externalChannelId as ExternalChannelId, - }) + const normalizeChannelLinkExternalId = (link: T) => ({ + ...link, + externalChannelId: link.externalChannelId as ExternalChannelId, + }) - const normalizeMessageLinkExternalId = (messageLink: T) => ({ - ...messageLink, - externalMessageId: messageLink.externalMessageId as ExternalMessageId, - }) + const normalizeMessageLinkExternalId = (messageLink: T) => ({ + ...messageLink, + externalMessageId: messageLink.externalMessageId as ExternalMessageId, + }) - const resolveExternalMessageId = Effect.fn("discordSyncWorker.resolveExternalMessageId")( - function* (params: { - syncConnectionId: SyncConnectionId - hazelMessageId: MessageId - preferredChannelLinkId?: SyncChannelLinkId - }) { - if (params.preferredChannelLinkId) { - const preferred = yield* messageLinkRepo.findByHazelMessage( - params.preferredChannelLinkId, - params.hazelMessageId, - ) + const resolveExternalMessageId = Effect.fn("discordSyncWorker.resolveExternalMessageId")( + function* (params: { + syncConnectionId: SyncConnectionId + hazelMessageId: MessageId + preferredChannelLinkId?: SyncChannelLinkId + }) { + if (params.preferredChannelLinkId) { + const preferred = yield* messageLinkRepo.findByHazelMessage( + params.preferredChannelLinkId, + params.hazelMessageId, + ) - if (Option.isSome(preferred)) { - return Option.some(preferred.value.externalMessageId as ExternalMessageId) + if (Option.isSome(preferred)) { + return Option.some(preferred.value.externalMessageId as ExternalMessageId) + } } - } - const links = yield* db.execute((client) => - client - .select({ - externalMessageId: schema.chatSyncMessageLinksTable.externalMessageId, - }) - .from(schema.chatSyncMessageLinksTable) - .innerJoin( - schema.chatSyncChannelLinksTable, - and( - eq( - schema.chatSyncChannelLinksTable.id, - schema.chatSyncMessageLinksTable.channelLinkId, + const links = yield* db.execute((client) => + client + .select({ + externalMessageId: schema.chatSyncMessageLinksTable.externalMessageId, + }) + .from(schema.chatSyncMessageLinksTable) + .innerJoin( + schema.chatSyncChannelLinksTable, + and( + eq( + schema.chatSyncChannelLinksTable.id, + schema.chatSyncMessageLinksTable.channelLinkId, + ), + eq( + schema.chatSyncChannelLinksTable.syncConnectionId, + params.syncConnectionId, + ), + isNull(schema.chatSyncChannelLinksTable.deletedAt), ), - eq(schema.chatSyncChannelLinksTable.syncConnectionId, params.syncConnectionId), - isNull(schema.chatSyncChannelLinksTable.deletedAt), - ), - ) - .where( - and( - eq(schema.chatSyncMessageLinksTable.hazelMessageId, params.hazelMessageId), - isNull(schema.chatSyncMessageLinksTable.deletedAt), - ), - ) - .limit(1), - ) - return Option.fromNullishOr(links[0]?.externalMessageId as ExternalMessageId | undefined) - }, - ) - - const resolveHazelMessageId = Effect.fn("discordSyncWorker.resolveHazelMessageId")(function* (params: { - syncConnectionId: SyncConnectionId - externalMessageId: ExternalMessageId - preferredChannelLinkId?: SyncChannelLinkId - }) { - if (params.preferredChannelLinkId) { - const preferred = yield* messageLinkRepo.findByExternalMessage( - params.preferredChannelLinkId, - params.externalMessageId, - ) - - if (Option.isSome(preferred)) { - return Option.some(preferred.value.hazelMessageId) - } - } - - const links = yield* db.execute((client) => - client - .select({ - hazelMessageId: schema.chatSyncMessageLinksTable.hazelMessageId, - }) - .from(schema.chatSyncMessageLinksTable) - .innerJoin( - schema.chatSyncChannelLinksTable, - and( - eq( - schema.chatSyncChannelLinksTable.id, - schema.chatSyncMessageLinksTable.channelLinkId, - ), - eq(schema.chatSyncChannelLinksTable.syncConnectionId, params.syncConnectionId), - isNull(schema.chatSyncChannelLinksTable.deletedAt), - ), - ) - .where( - and( - eq(schema.chatSyncMessageLinksTable.externalMessageId, params.externalMessageId), - isNull(schema.chatSyncMessageLinksTable.deletedAt), - ), + ) + .where( + and( + eq(schema.chatSyncMessageLinksTable.hazelMessageId, params.hazelMessageId), + isNull(schema.chatSyncMessageLinksTable.deletedAt), + ), + ) + .limit(1), ) - .limit(1), - ) - return Option.fromNullishOr(links[0]?.hazelMessageId) - }) - - const resolveOrCreateOutboundLinkForMessage = Effect.fn( - "discordSyncWorker.resolveOrCreateOutboundLinkForMessage", - )(function* (params: { - syncConnectionId: SyncConnectionId - provider: ChatSyncProvider - hazelChannelId: ChannelId - }) { - const directLink = yield* channelLinkRepo.findByHazelChannel( - params.syncConnectionId, - params.hazelChannelId, + return Option.fromNullishOr(links[0]?.externalMessageId as ExternalMessageId | undefined) + }, ) - if (Option.isSome(directLink)) { - return normalizeChannelLinkExternalId(directLink.value) - } - - const channelOption = yield* channelRepo.findById(params.hazelChannelId) - if (Option.isNone(channelOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: params.syncConnectionId, - }), - ) - } - const channel = channelOption.value - - if (channel.type !== "thread" || !channel.parentChannelId) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: params.syncConnectionId, - }), - ) - } - - const parentLinkOption = yield* channelLinkRepo.findByHazelChannel( - params.syncConnectionId, - channel.parentChannelId, - ) + const resolveHazelMessageId = Effect.fn("discordSyncWorker.resolveHazelMessageId")( + function* (params: { + syncConnectionId: SyncConnectionId + externalMessageId: ExternalMessageId + preferredChannelLinkId?: SyncChannelLinkId + }) { + if (params.preferredChannelLinkId) { + const preferred = yield* messageLinkRepo.findByExternalMessage( + params.preferredChannelLinkId, + params.externalMessageId, + ) - if (Option.isNone(parentLinkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: params.syncConnectionId, - }), - ) - } - const parentLink = parentLinkOption.value + if (Option.isSome(preferred)) { + return Option.some(preferred.value.hazelMessageId) + } + } - const rootMessages = yield* db.execute((client) => - client - .select({ - id: schema.messagesTable.id, - }) - .from(schema.messagesTable) - .where( - and( - eq(schema.messagesTable.threadChannelId, params.hazelChannelId), - isNull(schema.messagesTable.deletedAt), - ), + const links = yield* db.execute((client) => + client + .select({ + hazelMessageId: schema.chatSyncMessageLinksTable.hazelMessageId, + }) + .from(schema.chatSyncMessageLinksTable) + .innerJoin( + schema.chatSyncChannelLinksTable, + and( + eq( + schema.chatSyncChannelLinksTable.id, + schema.chatSyncMessageLinksTable.channelLinkId, + ), + eq( + schema.chatSyncChannelLinksTable.syncConnectionId, + params.syncConnectionId, + ), + isNull(schema.chatSyncChannelLinksTable.deletedAt), + ), + ) + .where( + and( + eq( + schema.chatSyncMessageLinksTable.externalMessageId, + params.externalMessageId, + ), + isNull(schema.chatSyncMessageLinksTable.deletedAt), + ), + ) + .limit(1), ) - .orderBy(asc(schema.messagesTable.createdAt), asc(schema.messagesTable.id)) - .limit(1), - ) - const rootMessageId = rootMessages[0]?.id - if (!rootMessageId) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: params.syncConnectionId, - }), - ) - } - - const rootMessageLinkOption = yield* messageLinkRepo.findByHazelMessage(parentLink.id, rootMessageId) - - if (Option.isNone(rootMessageLinkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: params.syncConnectionId, - }), - ) - } - - const adapter = yield* getProviderAdapter(params.provider) - const externalThreadId = yield* adapter.createThread({ - externalChannelId: normalizeChannelLinkExternalId(parentLink).externalChannelId, - externalMessageId: normalizeMessageLinkExternalId(rootMessageLinkOption.value).externalMessageId, - name: channel.name, - }) - const externalThreadChannelId = externalThreadId as unknown as ExternalChannelId - - const existingThreadLink = yield* channelLinkRepo.findByExternalChannel( - params.syncConnectionId, - externalThreadChannelId, + return Option.fromNullishOr(links[0]?.hazelMessageId) + }, ) - if (Option.isSome(existingThreadLink)) { - return normalizeChannelLinkExternalId(existingThreadLink.value) - } - - const [threadLink] = yield* channelLinkRepo.insert({ - syncConnectionId: params.syncConnectionId, - hazelChannelId: channel.id, - externalChannelId: externalThreadChannelId, - externalChannelName: channel.name, - direction: parentLink.direction, - isActive: true, - settings: parentLink.settings, - lastSyncedAt: null, - deletedAt: null, - }) - - yield* channelAccessSyncService.syncChannel(channel.id) - return normalizeChannelLinkExternalId(threadLink) - }) - - const syncHazelMessageToProvider = Effect.fn("discordSyncWorker.syncHazelMessageToProvider")(function* ( - syncConnectionId: SyncConnectionId, - hazelMessageId: MessageId, - dedupeKeyOverride?: string, - ) { - const dedupeKey = dedupeKeyOverride ?? `hazel:message:create:${hazelMessageId}` - const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) - if (!claimed) { - return { status: "deduped" as const } - } - - const connectionOption = yield* connectionRepo.findById(syncConnectionId) - - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId, - }), - ) - } - const connection = connectionOption.value - - const messageOption = yield* messageRepo.findById(hazelMessageId) - if (Option.isNone(messageOption)) { - return yield* Effect.fail( - new DiscordSyncMessageNotFoundError({ - messageId: hazelMessageId, - }), + const resolveOrCreateOutboundLinkForMessage = Effect.fn( + "discordSyncWorker.resolveOrCreateOutboundLinkForMessage", + )(function* (params: { + syncConnectionId: SyncConnectionId + provider: ChatSyncProvider + hazelChannelId: ChannelId + }) { + const directLink = yield* channelLinkRepo.findByHazelChannel( + params.syncConnectionId, + params.hazelChannelId, ) - } - const message = messageOption.value - const adapter = yield* getProviderAdapter(connection.provider) - const link = yield* resolveOrCreateOutboundLinkForMessage({ - syncConnectionId, - provider: connection.provider, - hazelChannelId: message.channelId, - }) - const normalizedLink = normalizeChannelLinkExternalId(link) - const existingMessageLink = yield* messageLinkRepo.findByHazelMessage(link.id, hazelMessageId) - - if (Option.isSome(existingMessageLink)) { - yield* writeReceipt({ - syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, - status: "ignored", - }) - return { status: "already_linked" as const } - } + if (Option.isSome(directLink)) { + return normalizeChannelLinkExternalId(directLink.value) + } - const replyToExternalMessageId = message.replyToMessageId - ? yield* resolveExternalMessageId({ - syncConnectionId, - hazelMessageId: message.replyToMessageId, - preferredChannelLinkId: link.id, - }).pipe( - Effect.map((id) => - Option.match(id, { - onNone: () => undefined, - onSome: (value) => value as ExternalMessageId, - }), - ), + const channelOption = yield* channelRepo.findById(params.hazelChannelId) + if (Option.isNone(channelOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: params.syncConnectionId, + }), ) - : undefined - const attachments = yield* listMessageAttachmentsForOutboundSync(message.id) - const outboundContent = - connection.provider === "discord" - ? yield* translateHazelMentionsForDiscord({ - organizationId: connection.organizationId, - content: message.content, - }) - : message.content - - let externalMessageId: ExternalMessageId - if (connection.provider === "discord") { - const webhookMessageId = yield* sendDiscordMessageViaWebhook({ - link: { - id: normalizedLink.id, - externalChannelId: normalizedLink.externalChannelId, - settings: link.settings, - }, - message: { - authorId: message.authorId, - }, - content: outboundContent, - attachments, - replyToExternalMessageId, - }) - - if (Option.isSome(webhookMessageId)) { - externalMessageId = webhookMessageId.value - } else if (attachments.length > 0) { - externalMessageId = yield* adapter.createMessageWithAttachments({ - externalChannelId: normalizedLink.externalChannelId, - content: outboundContent, - attachments, - replyToExternalMessageId, - }) - } else { - externalMessageId = yield* adapter.createMessage({ - externalChannelId: normalizedLink.externalChannelId, - content: outboundContent, - replyToExternalMessageId, - }) } - } else if (attachments.length > 0) { - externalMessageId = yield* adapter.createMessageWithAttachments({ - externalChannelId: normalizedLink.externalChannelId, - content: message.content, - attachments, - replyToExternalMessageId, - }) - } else { - externalMessageId = yield* adapter.createMessage({ - externalChannelId: normalizedLink.externalChannelId, - content: message.content, - replyToExternalMessageId, - }) - } - - yield* messageLinkRepo.insert({ - channelLinkId: link.id, - hazelMessageId: message.id, - externalMessageId, - source: "hazel", - rootHazelMessageId: null, - rootExternalMessageId: null, - hazelThreadChannelId: message.threadChannelId, - externalThreadId: null, - deletedAt: null, - }) - - yield* writeReceipt({ - syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, - payload: { - hazelMessageId, - externalMessageId, - }, - }) - yield* connectionRepo.updateLastSyncedAt(syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) + const channel = channelOption.value - return { status: "synced" as const, externalMessageId } - }) + if (channel.type !== "thread" || !channel.parentChannelId) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: params.syncConnectionId, + }), + ) + } - const syncConnection = Effect.fn("discordSyncWorker.syncConnection")(function* ( - syncConnectionId: SyncConnectionId, - maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, - ) { - const connectionOption = yield* connectionRepo.findById(syncConnectionId) - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId, - }), + const parentLinkOption = yield* channelLinkRepo.findByHazelChannel( + params.syncConnectionId, + channel.parentChannelId, ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - return { sent: 0, skipped: 0, failed: 0 } - } - const links = yield* channelLinkRepo.findActiveBySyncConnection(syncConnectionId) - - let sent = 0 - let skipped = 0 - let failed = 0 + if (Option.isNone(parentLinkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: params.syncConnectionId, + }), + ) + } + const parentLink = parentLinkOption.value - for (const link of links) { - const unsyncedMessages = yield* db.execute((client) => + const rootMessages = yield* db.execute((client) => client .select({ id: schema.messagesTable.id, }) .from(schema.messagesTable) - .leftJoin( - schema.chatSyncMessageLinksTable, - and( - eq(schema.chatSyncMessageLinksTable.channelLinkId, link.id), - eq(schema.chatSyncMessageLinksTable.hazelMessageId, schema.messagesTable.id), - isNull(schema.chatSyncMessageLinksTable.deletedAt), - ), - ) .where( and( - eq(schema.messagesTable.channelId, link.hazelChannelId), + eq(schema.messagesTable.threadChannelId, params.hazelChannelId), isNull(schema.messagesTable.deletedAt), - isNull(schema.chatSyncMessageLinksTable.id), ), ) .orderBy(asc(schema.messagesTable.createdAt), asc(schema.messagesTable.id)) - .limit(maxMessagesPerChannel), + .limit(1), ) - - for (const unsyncedMessage of unsyncedMessages) { - const result = yield* syncHazelMessageToProvider(syncConnectionId, unsyncedMessage.id).pipe( - Effect.result, + const rootMessageId = rootMessages[0]?.id + if (!rootMessageId) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: params.syncConnectionId, + }), ) - if (result._tag === "Success") { - if (result.success.status === "synced") { - sent++ - } else { - skipped++ - } - } else { - failed++ - yield* Effect.logWarning("Failed to sync Hazel message to provider", { - provider: connection.provider, - syncConnectionId, - hazelMessageId: unsyncedMessage.id, - error: result.failure, - }) - } } - } - - return { sent, skipped, failed } - }) - const syncHazelMessageUpdateToProvider = Effect.fn("discordSyncWorker.syncHazelMessageUpdateToProvider")( - function* ( - syncConnectionId: SyncConnectionId, - hazelMessageId: MessageId, - dedupeKeyOverride?: string, - ) { - const dedupeKey = dedupeKeyOverride ?? `hazel:message:update:${hazelMessageId}` - const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) - if (!claimed) { - return { status: "deduped" as const } - } + const rootMessageLinkOption = yield* messageLinkRepo.findByHazelMessage( + parentLink.id, + rootMessageId, + ) - const connectionOption = yield* connectionRepo.findById(syncConnectionId) - if (Option.isNone(connectionOption)) { + if (Option.isNone(rootMessageLinkOption)) { return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId, + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: params.syncConnectionId, }), ) } - const connection = connectionOption.value - const adapter = yield* getProviderAdapter(connection.provider) - const messageOption = yield* messageRepo.findById(hazelMessageId) - if (Option.isNone(messageOption)) { - return yield* Effect.fail(new DiscordSyncMessageNotFoundError({ messageId: hazelMessageId })) + const adapter = yield* getProviderAdapter(params.provider) + const externalThreadId = yield* adapter.createThread({ + externalChannelId: normalizeChannelLinkExternalId(parentLink).externalChannelId, + externalMessageId: normalizeMessageLinkExternalId(rootMessageLinkOption.value) + .externalMessageId, + name: channel.name, + }) + const externalThreadChannelId = externalThreadId as unknown as ExternalChannelId + + const existingThreadLink = yield* channelLinkRepo.findByExternalChannel( + params.syncConnectionId, + externalThreadChannelId, + ) + + if (Option.isSome(existingThreadLink)) { + return normalizeChannelLinkExternalId(existingThreadLink.value) } - const message = messageOption.value - const link = yield* resolveOrCreateOutboundLinkForMessage({ - syncConnectionId, - provider: connection.provider, - hazelChannelId: message.channelId, + const [threadLink] = yield* channelLinkRepo.insert({ + syncConnectionId: params.syncConnectionId, + hazelChannelId: channel.id, + externalChannelId: externalThreadChannelId, + externalChannelName: channel.name, + direction: parentLink.direction, + isActive: true, + settings: parentLink.settings, + lastSyncedAt: null, + deletedAt: null, }) - const normalizedLink = normalizeChannelLinkExternalId(link) - const messageLinkOption = yield* messageLinkRepo.findByHazelMessage(link.id, hazelMessageId) + yield* channelAccessSyncService.syncChannel(channel.id) + return normalizeChannelLinkExternalId(threadLink) + }) - if (Option.isNone(messageLinkOption)) { - yield* writeReceipt({ + const syncHazelMessageToProvider = Effect.fn("discordSyncWorker.syncHazelMessageToProvider")( + function* ( + syncConnectionId: SyncConnectionId, + hazelMessageId: MessageId, + dedupeKeyOverride?: string, + ) { + const dedupeKey = dedupeKeyOverride ?? `hazel:message:create:${hazelMessageId}` + const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) + if (!claimed) { + return { status: "deduped" as const } + } + + const connectionOption = yield* connectionRepo.findById(syncConnectionId) + + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId, + }), + ) + } + const connection = connectionOption.value + + const messageOption = yield* messageRepo.findById(hazelMessageId) + if (Option.isNone(messageOption)) { + return yield* Effect.fail( + new DiscordSyncMessageNotFoundError({ + messageId: hazelMessageId, + }), + ) + } + const message = messageOption.value + const adapter = yield* getProviderAdapter(connection.provider) + const link = yield* resolveOrCreateOutboundLinkForMessage({ syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, - status: "ignored", - payload: { hazelMessageId }, + provider: connection.provider, + hazelChannelId: message.channelId, }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - const normalizedMessageLink = normalizeMessageLinkExternalId(messageLink) - const outboundContent = - connection.provider === "discord" - ? yield* translateHazelMentionsForDiscord({ - organizationId: connection.organizationId, - content: message.content, + const normalizedLink = normalizeChannelLinkExternalId(link) + + const existingMessageLink = yield* messageLinkRepo.findByHazelMessage(link.id, hazelMessageId) + + if (Option.isSome(existingMessageLink)) { + yield* writeReceipt({ + syncConnectionId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, + status: "ignored", + }) + return { status: "already_linked" as const } + } + + const replyToExternalMessageId = message.replyToMessageId + ? yield* resolveExternalMessageId({ + syncConnectionId, + hazelMessageId: message.replyToMessageId, + preferredChannelLinkId: link.id, + }).pipe( + Effect.map((id) => + Option.match(id, { + onNone: () => undefined, + onSome: (value) => value as ExternalMessageId, + }), + ), + ) + : undefined + const attachments = yield* listMessageAttachmentsForOutboundSync(message.id) + const outboundContent = + connection.provider === "discord" + ? yield* translateHazelMentionsForDiscord({ + organizationId: connection.organizationId, + content: message.content, + }) + : message.content + + let externalMessageId: ExternalMessageId + if (connection.provider === "discord") { + const webhookMessageId = yield* sendDiscordMessageViaWebhook({ + link: { + id: normalizedLink.id, + externalChannelId: normalizedLink.externalChannelId, + settings: link.settings, + }, + message: { + authorId: message.authorId, + }, + content: outboundContent, + attachments, + replyToExternalMessageId, + }) + + if (Option.isSome(webhookMessageId)) { + externalMessageId = webhookMessageId.value + } else if (attachments.length > 0) { + externalMessageId = yield* adapter.createMessageWithAttachments({ + externalChannelId: normalizedLink.externalChannelId, + content: outboundContent, + attachments, + replyToExternalMessageId, + }) + } else { + externalMessageId = yield* adapter.createMessage({ + externalChannelId: normalizedLink.externalChannelId, + content: outboundContent, + replyToExternalMessageId, + }) + } + } else if (attachments.length > 0) { + externalMessageId = yield* adapter.createMessageWithAttachments({ + externalChannelId: normalizedLink.externalChannelId, + content: message.content, + attachments, + replyToExternalMessageId, + }) + } else { + externalMessageId = yield* adapter.createMessage({ + externalChannelId: normalizedLink.externalChannelId, + content: message.content, + replyToExternalMessageId, + }) + } + + yield* messageLinkRepo.insert({ + channelLinkId: link.id, + hazelMessageId: message.id, + externalMessageId, + source: "hazel", + rootHazelMessageId: null, + rootExternalMessageId: null, + hazelThreadChannelId: message.threadChannelId, + externalThreadId: null, + deletedAt: null, + }) + + yield* writeReceipt({ + syncConnectionId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, + payload: { + hazelMessageId, + externalMessageId, + }, + }) + yield* connectionRepo.updateLastSyncedAt(syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) + + return { status: "synced" as const, externalMessageId } + }, + ) + + const syncConnection = Effect.fn("discordSyncWorker.syncConnection")(function* ( + syncConnectionId: SyncConnectionId, + maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, + ) { + const connectionOption = yield* connectionRepo.findById(syncConnectionId) + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId, + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + return { sent: 0, skipped: 0, failed: 0 } + } + + const links = yield* channelLinkRepo.findActiveBySyncConnection(syncConnectionId) + + let sent = 0 + let skipped = 0 + let failed = 0 + + for (const link of links) { + const unsyncedMessages = yield* db.execute((client) => + client + .select({ + id: schema.messagesTable.id, + }) + .from(schema.messagesTable) + .leftJoin( + schema.chatSyncMessageLinksTable, + and( + eq(schema.chatSyncMessageLinksTable.channelLinkId, link.id), + eq(schema.chatSyncMessageLinksTable.hazelMessageId, schema.messagesTable.id), + isNull(schema.chatSyncMessageLinksTable.deletedAt), + ), + ) + .where( + and( + eq(schema.messagesTable.channelId, link.hazelChannelId), + isNull(schema.messagesTable.deletedAt), + isNull(schema.chatSyncMessageLinksTable.id), + ), + ) + .orderBy(asc(schema.messagesTable.createdAt), asc(schema.messagesTable.id)) + .limit(maxMessagesPerChannel), + ) + + for (const unsyncedMessage of unsyncedMessages) { + const result = yield* syncHazelMessageToProvider( + syncConnectionId, + unsyncedMessage.id, + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.success.status === "synced") { + sent++ + } else { + skipped++ + } + } else { + failed++ + yield* Effect.logWarning("Failed to sync Hazel message to provider", { + provider: connection.provider, + syncConnectionId, + hazelMessageId: unsyncedMessage.id, + error: result.failure, + }) + } + } + } + + return { sent, skipped, failed } + }) + + const syncHazelMessageUpdateToProvider = Effect.fn( + "discordSyncWorker.syncHazelMessageUpdateToProvider", + )(function* ( + syncConnectionId: SyncConnectionId, + hazelMessageId: MessageId, + dedupeKeyOverride?: string, + ) { + const dedupeKey = dedupeKeyOverride ?? `hazel:message:update:${hazelMessageId}` + const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) + if (!claimed) { + return { status: "deduped" as const } + } + + const connectionOption = yield* connectionRepo.findById(syncConnectionId) + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId, + }), + ) + } + const connection = connectionOption.value + const adapter = yield* getProviderAdapter(connection.provider) + + const messageOption = yield* messageRepo.findById(hazelMessageId) + if (Option.isNone(messageOption)) { + return yield* Effect.fail(new DiscordSyncMessageNotFoundError({ messageId: hazelMessageId })) + } + const message = messageOption.value + + const link = yield* resolveOrCreateOutboundLinkForMessage({ + syncConnectionId, + provider: connection.provider, + hazelChannelId: message.channelId, + }) + const normalizedLink = normalizeChannelLinkExternalId(link) + + const messageLinkOption = yield* messageLinkRepo.findByHazelMessage(link.id, hazelMessageId) + + if (Option.isNone(messageLinkOption)) { + yield* writeReceipt({ + syncConnectionId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, + status: "ignored", + payload: { hazelMessageId }, + }) + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + const normalizedMessageLink = normalizeMessageLinkExternalId(messageLink) + const outboundContent = + connection.provider === "discord" + ? yield* translateHazelMentionsForDiscord({ + organizationId: connection.organizationId, + content: message.content, }) : message.content @@ -1387,11 +1414,11 @@ export const ChatSyncCoreWorkerMake: Effect.Effect, unk status: "updated" as const, externalMessageId: normalizedMessageLink.externalMessageId, } - }, - ) + }) - const syncHazelMessageDeleteToProvider = Effect.fn("discordSyncWorker.syncHazelMessageDeleteToProvider")( - function* ( + const syncHazelMessageDeleteToProvider = Effect.fn( + "discordSyncWorker.syncHazelMessageDeleteToProvider", + )(function* ( syncConnectionId: SyncConnectionId, hazelMessageId: MessageId, dedupeKeyOverride?: string, @@ -1483,1266 +1510,1272 @@ export const ChatSyncCoreWorkerMake: Effect.Effect, unk status: "deleted" as const, externalMessageId: normalizedMessageLink.externalMessageId, } - }, - ) + }) - const syncHazelReactionCreateToProvider = Effect.fn( - "discordSyncWorker.syncHazelReactionCreateToProvider", - )(function* ( - syncConnectionId: SyncConnectionId, - hazelReactionId: MessageReactionId, - dedupeKeyOverride?: string, - ) { - const dedupeKey = dedupeKeyOverride ?? `hazel:reaction:create:${hazelReactionId}` - const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) - if (!claimed) { - return { status: "deduped" as const } - } + const syncHazelReactionCreateToProvider = Effect.fn( + "discordSyncWorker.syncHazelReactionCreateToProvider", + )(function* ( + syncConnectionId: SyncConnectionId, + hazelReactionId: MessageReactionId, + dedupeKeyOverride?: string, + ) { + const dedupeKey = dedupeKeyOverride ?? `hazel:reaction:create:${hazelReactionId}` + const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) + if (!claimed) { + return { status: "deduped" as const } + } + + const connectionOption = yield* connectionRepo.findById(syncConnectionId) + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId, + }), + ) + } + const connection = connectionOption.value + const adapter = yield* getProviderAdapter(connection.provider) - const connectionOption = yield* connectionRepo.findById(syncConnectionId) - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ + const reactionOption = yield* messageReactionRepo.findById(hazelReactionId) + if (Option.isNone(reactionOption)) { + yield* writeReceipt({ syncConnectionId, - }), - ) - } - const connection = connectionOption.value - const adapter = yield* getProviderAdapter(connection.provider) + source: "hazel", + dedupeKey, + status: "ignored", + }) + return { status: "ignored_missing_reaction" as const } + } + const reaction = reactionOption.value - const reactionOption = yield* messageReactionRepo.findById(hazelReactionId) - if (Option.isNone(reactionOption)) { - yield* writeReceipt({ + const link = yield* resolveOrCreateOutboundLinkForMessage({ syncConnectionId, - source: "hazel", - dedupeKey, - status: "ignored", + provider: connection.provider, + hazelChannelId: reaction.channelId, }) - return { status: "ignored_missing_reaction" as const } - } - const reaction = reactionOption.value + const normalizedLink = normalizeChannelLinkExternalId(link) - const link = yield* resolveOrCreateOutboundLinkForMessage({ - syncConnectionId, - provider: connection.provider, - hazelChannelId: reaction.channelId, - }) - const normalizedLink = normalizeChannelLinkExternalId(link) + const messageLinkOption = yield* messageLinkRepo.findByHazelMessage(link.id, reaction.messageId) + + if (Option.isNone(messageLinkOption)) { + yield* writeReceipt({ + syncConnectionId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, + status: "ignored", + payload: { + hazelReactionId, + }, + }) + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + const normalizedMessageLink = normalizeMessageLinkExternalId(messageLink) - const messageLinkOption = yield* messageLinkRepo.findByHazelMessage(link.id, reaction.messageId) + yield* adapter.addReaction({ + externalChannelId: normalizedLink.externalChannelId, + externalMessageId: normalizedMessageLink.externalMessageId, + emoji: reaction.emoji, + }) - if (Option.isNone(messageLinkOption)) { yield* writeReceipt({ syncConnectionId, channelLinkId: link.id, source: "hazel", dedupeKey, - status: "ignored", payload: { hazelReactionId, + externalMessageId: normalizedMessageLink.externalMessageId, + emoji: reaction.emoji, }, }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - const normalizedMessageLink = normalizeMessageLinkExternalId(messageLink) + yield* connectionRepo.updateLastSyncedAt(syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) - yield* adapter.addReaction({ - externalChannelId: normalizedLink.externalChannelId, - externalMessageId: normalizedMessageLink.externalMessageId, - emoji: reaction.emoji, + return { + status: "created" as const, + externalMessageId: normalizedMessageLink.externalMessageId, + } }) - yield* writeReceipt({ - syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, + const syncHazelReactionDeleteToProvider = Effect.fn( + "discordSyncWorker.syncHazelReactionDeleteToProvider", + )(function* ( + syncConnectionId: SyncConnectionId, payload: { - hazelReactionId, - externalMessageId: normalizedMessageLink.externalMessageId, - emoji: reaction.emoji, + hazelChannelId: ChannelId + hazelMessageId: MessageId + emoji: string + userId?: UserId }, - }) - yield* connectionRepo.updateLastSyncedAt(syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) - - return { - status: "created" as const, - externalMessageId: normalizedMessageLink.externalMessageId, - } - }) - - const syncHazelReactionDeleteToProvider = Effect.fn( - "discordSyncWorker.syncHazelReactionDeleteToProvider", - )(function* ( - syncConnectionId: SyncConnectionId, - payload: { - hazelChannelId: ChannelId - hazelMessageId: MessageId - emoji: string - userId?: UserId - }, - dedupeKeyOverride?: string, - ) { - const dedupeKey = - dedupeKeyOverride ?? - `hazel:reaction:delete:${payload.hazelMessageId}:${payload.emoji}:${payload.userId ?? "unknown"}` - const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) - if (!claimed) { - return { status: "deduped" as const } - } - - const connectionOption = yield* connectionRepo.findById(syncConnectionId) - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId, - }), - ) - } - const connection = connectionOption.value - const adapter = yield* getProviderAdapter(connection.provider) - - const link = yield* resolveOrCreateOutboundLinkForMessage({ - syncConnectionId, - provider: connection.provider, - hazelChannelId: payload.hazelChannelId, - }) - const normalizedLink = normalizeChannelLinkExternalId(link) + dedupeKeyOverride?: string, + ) { + const dedupeKey = + dedupeKeyOverride ?? + `hazel:reaction:delete:${payload.hazelMessageId}:${payload.emoji}:${payload.userId ?? "unknown"}` + const claimed = yield* claimReceipt({ syncConnectionId, source: "hazel", dedupeKey }) + if (!claimed) { + return { status: "deduped" as const } + } - const messageLinkOption = yield* messageLinkRepo.findByHazelMessage(link.id, payload.hazelMessageId) + const connectionOption = yield* connectionRepo.findById(syncConnectionId) + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId, + }), + ) + } + const connection = connectionOption.value + const adapter = yield* getProviderAdapter(connection.provider) - if (Option.isNone(messageLinkOption)) { - yield* writeReceipt({ + const link = yield* resolveOrCreateOutboundLinkForMessage({ syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, - status: "ignored", - payload, + provider: connection.provider, + hazelChannelId: payload.hazelChannelId, }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - const normalizedMessageLink = normalizeMessageLinkExternalId(messageLink) + const normalizedLink = normalizeChannelLinkExternalId(link) - const remainingReactions = yield* db.execute((client) => - client - .select({ - id: schema.messageReactionsTable.id, - }) - .from(schema.messageReactionsTable) - .where( - and( - eq(schema.messageReactionsTable.messageId, payload.hazelMessageId), - eq(schema.messageReactionsTable.emoji, payload.emoji), - ), - ) - .limit(1), - ) - if (remainingReactions.length > 0) { - yield* writeReceipt({ - syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, - status: "ignored", - payload: { - ...payload, - reason: "remaining_reactions_for_emoji", - }, - }) - return { status: "ignored_remaining_reactions" as const } - } + const messageLinkOption = yield* messageLinkRepo.findByHazelMessage( + link.id, + payload.hazelMessageId, + ) - yield* adapter.removeReaction({ - externalChannelId: normalizedLink.externalChannelId, - externalMessageId: normalizedMessageLink.externalMessageId, - emoji: payload.emoji, - }) + if (Option.isNone(messageLinkOption)) { + yield* writeReceipt({ + syncConnectionId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + const normalizedMessageLink = normalizeMessageLinkExternalId(messageLink) - yield* writeReceipt({ - syncConnectionId, - channelLinkId: link.id, - source: "hazel", - dedupeKey, - payload: { - ...payload, + const remainingReactions = yield* db.execute((client) => + client + .select({ + id: schema.messageReactionsTable.id, + }) + .from(schema.messageReactionsTable) + .where( + and( + eq(schema.messageReactionsTable.messageId, payload.hazelMessageId), + eq(schema.messageReactionsTable.emoji, payload.emoji), + ), + ) + .limit(1), + ) + if (remainingReactions.length > 0) { + yield* writeReceipt({ + syncConnectionId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, + status: "ignored", + payload: { + ...payload, + reason: "remaining_reactions_for_emoji", + }, + }) + return { status: "ignored_remaining_reactions" as const } + } + + yield* adapter.removeReaction({ + externalChannelId: normalizedLink.externalChannelId, externalMessageId: normalizedMessageLink.externalMessageId, - }, + emoji: payload.emoji, + }) + + yield* writeReceipt({ + syncConnectionId, + channelLinkId: link.id, + source: "hazel", + dedupeKey, + payload: { + ...payload, + externalMessageId: normalizedMessageLink.externalMessageId, + }, + }) + yield* connectionRepo.updateLastSyncedAt(syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) + + return { + status: "deleted" as const, + externalMessageId: normalizedMessageLink.externalMessageId, + } }) - yield* connectionRepo.updateLastSyncedAt(syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) - return { - status: "deleted" as const, - externalMessageId: normalizedMessageLink.externalMessageId, - } - }) - - const getActiveOutboundTargets = Effect.fn("discordSyncWorker.getActiveOutboundTargets")(function* ( - hazelChannelId: ChannelId, - provider: ChatSyncProvider, - ) { - const targets = yield* db.execute((client) => - client - .select({ - syncConnectionId: schema.chatSyncConnectionsTable.id, - channelLinkId: schema.chatSyncChannelLinksTable.id, - direction: schema.chatSyncChannelLinksTable.direction, - }) - .from(schema.chatSyncChannelLinksTable) - .innerJoin( - schema.chatSyncConnectionsTable, - eq(schema.chatSyncConnectionsTable.id, schema.chatSyncChannelLinksTable.syncConnectionId), - ) - .where( - and( - eq(schema.chatSyncChannelLinksTable.hazelChannelId, hazelChannelId), - eq(schema.chatSyncChannelLinksTable.isActive, true), - isNull(schema.chatSyncChannelLinksTable.deletedAt), - eq(schema.chatSyncConnectionsTable.provider, provider), - eq(schema.chatSyncConnectionsTable.status, "active"), - isNull(schema.chatSyncConnectionsTable.deletedAt), + const getActiveOutboundTargets = Effect.fn("discordSyncWorker.getActiveOutboundTargets")(function* ( + hazelChannelId: ChannelId, + provider: ChatSyncProvider, + ) { + const targets = yield* db.execute((client) => + client + .select({ + syncConnectionId: schema.chatSyncConnectionsTable.id, + channelLinkId: schema.chatSyncChannelLinksTable.id, + direction: schema.chatSyncChannelLinksTable.direction, + }) + .from(schema.chatSyncChannelLinksTable) + .innerJoin( + schema.chatSyncConnectionsTable, + eq( + schema.chatSyncConnectionsTable.id, + schema.chatSyncChannelLinksTable.syncConnectionId, + ), + ) + .where( + and( + eq(schema.chatSyncChannelLinksTable.hazelChannelId, hazelChannelId), + eq(schema.chatSyncChannelLinksTable.isActive, true), + isNull(schema.chatSyncChannelLinksTable.deletedAt), + eq(schema.chatSyncConnectionsTable.provider, provider), + eq(schema.chatSyncConnectionsTable.status, "active"), + isNull(schema.chatSyncConnectionsTable.deletedAt), + ), ), - ), - ) - return targets - }) - - const syncHazelMessageCreateToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelMessageCreateToAllConnections", - )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { - const messageOption = yield* messageRepo.findById(hazelMessageId) - if (Option.isNone(messageOption)) { - return { synced: 0, failed: 0 } - } - const targets = yield* getActiveOutboundTargets(messageOption.value.channelId, provider) - let synced = 0 - let failed = 0 - - for (const target of targets) { - if (target.direction === "external_to_hazel") continue - const result = yield* syncHazelMessageToProvider( - target.syncConnectionId, - hazelMessageId, - dedupeKey, - ).pipe(Effect.result) - if (result._tag === "Success") { - if (result.success.status === "synced" || result.success.status === "already_linked") { - synced++ - } - } else { - failed++ - yield* Effect.logWarning("Failed to sync create message to provider", { - provider, + ) + return targets + }) + + const syncHazelMessageCreateToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelMessageCreateToAllConnections", + )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { + const messageOption = yield* messageRepo.findById(hazelMessageId) + if (Option.isNone(messageOption)) { + return { synced: 0, failed: 0 } + } + const targets = yield* getActiveOutboundTargets(messageOption.value.channelId, provider) + let synced = 0 + let failed = 0 + + for (const target of targets) { + if (target.direction === "external_to_hazel") continue + const result = yield* syncHazelMessageToProvider( + target.syncConnectionId, hazelMessageId, - syncConnectionId: target.syncConnectionId, - error: result.failure, - }) + dedupeKey, + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.success.status === "synced" || result.success.status === "already_linked") { + synced++ + } + } else { + failed++ + yield* Effect.logWarning("Failed to sync create message to provider", { + provider, + hazelMessageId, + syncConnectionId: target.syncConnectionId, + error: result.failure, + }) + } } - } - return { synced, failed } - }) + return { synced, failed } + }) - const syncHazelMessageUpdateToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelMessageUpdateToAllConnections", - )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { - const messageOption = yield* messageRepo.findById(hazelMessageId) - if (Option.isNone(messageOption)) { - return { synced: 0, failed: 0 } - } - const targets = yield* getActiveOutboundTargets(messageOption.value.channelId, provider) - let synced = 0 - let failed = 0 - - for (const target of targets) { - if (target.direction === "external_to_hazel") continue - const result = yield* syncHazelMessageUpdateToProvider( - target.syncConnectionId, - hazelMessageId, - dedupeKey, - ).pipe(Effect.result) - if (result._tag === "Success") { - if (result.success.status === "updated") { - synced++ - } - } else { - failed++ - yield* Effect.logWarning("Failed to sync update message to provider", { - provider, + const syncHazelMessageUpdateToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelMessageUpdateToAllConnections", + )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { + const messageOption = yield* messageRepo.findById(hazelMessageId) + if (Option.isNone(messageOption)) { + return { synced: 0, failed: 0 } + } + const targets = yield* getActiveOutboundTargets(messageOption.value.channelId, provider) + let synced = 0 + let failed = 0 + + for (const target of targets) { + if (target.direction === "external_to_hazel") continue + const result = yield* syncHazelMessageUpdateToProvider( + target.syncConnectionId, hazelMessageId, - syncConnectionId: target.syncConnectionId, - error: result.failure, - }) + dedupeKey, + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.success.status === "updated") { + synced++ + } + } else { + failed++ + yield* Effect.logWarning("Failed to sync update message to provider", { + provider, + hazelMessageId, + syncConnectionId: target.syncConnectionId, + error: result.failure, + }) + } } - } - return { synced, failed } - }) + return { synced, failed } + }) - const syncHazelMessageDeleteToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelMessageDeleteToAllConnections", - )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { - const messageOption = yield* messageRepo.findById(hazelMessageId) - if (Option.isNone(messageOption)) { - return { synced: 0, failed: 0 } - } - const targets = yield* getActiveOutboundTargets(messageOption.value.channelId, provider) - let synced = 0 - let failed = 0 - - for (const target of targets) { - if (target.direction === "external_to_hazel") continue - const result = yield* syncHazelMessageDeleteToProvider( - target.syncConnectionId, - hazelMessageId, - dedupeKey, - ).pipe(Effect.result) - if (result._tag === "Success") { - if (result.success.status === "deleted") { - synced++ - } - } else { - failed++ - yield* Effect.logWarning("Failed to sync delete message to provider", { - provider, + const syncHazelMessageDeleteToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelMessageDeleteToAllConnections", + )(function* (provider: ChatSyncProvider, hazelMessageId: MessageId, dedupeKey?: string) { + const messageOption = yield* messageRepo.findById(hazelMessageId) + if (Option.isNone(messageOption)) { + return { synced: 0, failed: 0 } + } + const targets = yield* getActiveOutboundTargets(messageOption.value.channelId, provider) + let synced = 0 + let failed = 0 + + for (const target of targets) { + if (target.direction === "external_to_hazel") continue + const result = yield* syncHazelMessageDeleteToProvider( + target.syncConnectionId, hazelMessageId, - syncConnectionId: target.syncConnectionId, - error: result.failure, - }) + dedupeKey, + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.success.status === "deleted") { + synced++ + } + } else { + failed++ + yield* Effect.logWarning("Failed to sync delete message to provider", { + provider, + hazelMessageId, + syncConnectionId: target.syncConnectionId, + error: result.failure, + }) + } } - } - return { synced, failed } - }) + return { synced, failed } + }) - const syncHazelReactionCreateToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelReactionCreateToAllConnections", - )(function* (provider: ChatSyncProvider, hazelReactionId: MessageReactionId, dedupeKey?: string) { - const reactionOption = yield* messageReactionRepo.findById(hazelReactionId) - if (Option.isNone(reactionOption)) { - return { synced: 0, failed: 0 } - } - const reaction = reactionOption.value - const targets = yield* getActiveOutboundTargets(reaction.channelId, provider) - let synced = 0 - let failed = 0 - - for (const target of targets) { - if (target.direction === "external_to_hazel") continue - const result = yield* syncHazelReactionCreateToProvider( - target.syncConnectionId, - hazelReactionId, - dedupeKey, - ).pipe(Effect.result) - if (result._tag === "Success") { - if (result.success.status === "created") { - synced++ - } - } else { - failed++ - yield* Effect.logWarning("Failed to sync create reaction to provider", { - provider, + const syncHazelReactionCreateToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelReactionCreateToAllConnections", + )(function* (provider: ChatSyncProvider, hazelReactionId: MessageReactionId, dedupeKey?: string) { + const reactionOption = yield* messageReactionRepo.findById(hazelReactionId) + if (Option.isNone(reactionOption)) { + return { synced: 0, failed: 0 } + } + const reaction = reactionOption.value + const targets = yield* getActiveOutboundTargets(reaction.channelId, provider) + let synced = 0 + let failed = 0 + + for (const target of targets) { + if (target.direction === "external_to_hazel") continue + const result = yield* syncHazelReactionCreateToProvider( + target.syncConnectionId, hazelReactionId, - syncConnectionId: target.syncConnectionId, - error: result.failure, - }) + dedupeKey, + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.success.status === "created") { + synced++ + } + } else { + failed++ + yield* Effect.logWarning("Failed to sync create reaction to provider", { + provider, + hazelReactionId, + syncConnectionId: target.syncConnectionId, + error: result.failure, + }) + } } - } - return { synced, failed } - }) + return { synced, failed } + }) - const syncHazelReactionDeleteToAllConnections = Effect.fn( - "discordSyncWorker.syncHazelReactionDeleteToAllConnections", - )(function* ( - provider: ChatSyncProvider, - payload: { - hazelChannelId: ChannelId - hazelMessageId: MessageId - emoji: string - userId?: UserId - }, - dedupeKey?: string, - ) { - const targets = yield* getActiveOutboundTargets(payload.hazelChannelId, provider) - let synced = 0 - let failed = 0 - - for (const target of targets) { - if (target.direction === "external_to_hazel") continue - const result = yield* syncHazelReactionDeleteToProvider( - target.syncConnectionId, - payload, - dedupeKey, - ).pipe(Effect.result) - if (result._tag === "Success") { - if (result.success.status === "deleted") { - synced++ + const syncHazelReactionDeleteToAllConnections = Effect.fn( + "discordSyncWorker.syncHazelReactionDeleteToAllConnections", + )(function* ( + provider: ChatSyncProvider, + payload: { + hazelChannelId: ChannelId + hazelMessageId: MessageId + emoji: string + userId?: UserId + }, + dedupeKey?: string, + ) { + const targets = yield* getActiveOutboundTargets(payload.hazelChannelId, provider) + let synced = 0 + let failed = 0 + + for (const target of targets) { + if (target.direction === "external_to_hazel") continue + const result = yield* syncHazelReactionDeleteToProvider( + target.syncConnectionId, + payload, + dedupeKey, + ).pipe(Effect.result) + if (result._tag === "Success") { + if (result.success.status === "deleted") { + synced++ + } + } else { + failed++ + yield* Effect.logWarning("Failed to sync delete reaction to provider", { + provider, + hazelMessageId: payload.hazelMessageId, + syncConnectionId: target.syncConnectionId, + error: result.failure, + }) } - } else { - failed++ - yield* Effect.logWarning("Failed to sync delete reaction to provider", { - provider, - hazelMessageId: payload.hazelMessageId, - syncConnectionId: target.syncConnectionId, - error: result.failure, - }) } - } - return { synced, failed } - }) - - const syncAllActiveConnections = Effect.fn("discordSyncWorker.syncAllActiveConnections")(function* ( - provider: ChatSyncProvider, - maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, - ) { - const connections = yield* connectionRepo.findActiveByProvider(provider) - return yield* Effect.forEach( - connections, - (connection) => - syncConnection(connection.id, maxMessagesPerChannel).pipe( - Effect.map((summary) => ({ - syncConnectionId: connection.id, - ...summary, - })), - ), - { concurrency: DEFAULT_CHAT_SYNC_CONCURRENCY }, - ) - }) - - const ingestMessageCreate = Effect.fn("discordSyncWorker.ingestMessageCreate")(function* ( - payload: ChatSyncIngressMessageCreate, - ) { - const dedupeKey = payload.dedupeKey ?? `external:message:create:${payload.externalMessageId}` - const claimed = yield* claimReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, + return { synced, failed } }) - if (!claimed) { - return { status: "deduped" as const } - } - - const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId: payload.syncConnectionId, - }), + const syncAllActiveConnections = Effect.fn("discordSyncWorker.syncAllActiveConnections")(function* ( + provider: ChatSyncProvider, + maxMessagesPerChannel = DEFAULT_MAX_MESSAGES_PER_CHANNEL, + ) { + const connections = yield* connectionRepo.findActiveByProvider(provider) + return yield* Effect.forEach( + connections, + (connection) => + syncConnection(connection.id, maxMessagesPerChannel).pipe( + Effect.map((summary) => ({ + syncConnectionId: connection.id, + ...summary, + })), + ), + { concurrency: DEFAULT_CHAT_SYNC_CONCURRENCY }, ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - yield* writeReceipt({ + }) + + const ingestMessageCreate = Effect.fn("discordSyncWorker.ingestMessageCreate")(function* ( + payload: ChatSyncIngressMessageCreate, + ) { + const dedupeKey = payload.dedupeKey ?? `external:message:create:${payload.externalMessageId}` + const claimed = yield* claimReceipt({ syncConnectionId: payload.syncConnectionId, source: "external", dedupeKey, - status: "ignored", - payload, }) - return { status: "ignored_connection_inactive" as const } - } - yield* getProviderAdapter(connection.provider) + if (!claimed) { + return { status: "deduped" as const } + } - const linkOption = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - payload.externalChannelId, - ) + const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) - if (Option.isNone(linkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId: payload.syncConnectionId, + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, - externalChannelId: payload.externalChannelId, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "ignored_connection_inactive" as const } + } + yield* getProviderAdapter(connection.provider) + + const linkOption = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + payload.externalChannelId, + ) + + if (Option.isNone(linkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: payload.syncConnectionId, + externalChannelId: payload.externalChannelId, + }), + ) + } + const link = linkOption.value + if (shouldIgnoreWebhookOrigin(connection.provider, link.settings, payload.externalWebhookId)) { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "ignored_webhook_origin" as const } + } + + const existingMessageLink = yield* messageLinkRepo.findByExternalMessage( + link.id, + payload.externalMessageId, + ) + + if (Option.isSome(existingMessageLink)) { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, + source: "external", + dedupeKey, + status: "ignored", + }) + return { status: "already_linked" as const } + } + + const authorId = payload.externalAuthorId + ? yield* resolveAuthorUserId({ + provider: connection.provider, + organizationId: connection.organizationId, + externalUserId: payload.externalAuthorId, + displayName: payload.externalAuthorDisplayName ?? "External User", + avatarUrl: payload.externalAuthorAvatarUrl ?? null, + }) + : (yield* integrationBotService.getOrCreateBotUser( + decodeProvider(connection.provider), + connection.organizationId, + )).id + const replyToMessageId = payload.externalReplyToMessageId + ? yield* resolveHazelMessageId({ + syncConnectionId: payload.syncConnectionId, + externalMessageId: payload.externalReplyToMessageId, + preferredChannelLinkId: link.id, + }).pipe( + Effect.map((id) => + Option.match(id, { + onNone: () => null, + onSome: (value) => value, + }), + ), + ) + : null + const normalizedExternalAttachments: Array = [] + for (const attachment of payload.externalAttachments ?? []) { + const fileName = attachment.fileName.trim() + const publicUrl = attachment.publicUrl.trim() + if (!fileName || !publicUrl) { + continue + } + normalizedExternalAttachments.push({ + externalAttachmentId: attachment.externalAttachmentId, + fileName, + fileSize: + Number.isFinite(attachment.fileSize) && attachment.fileSize >= 0 + ? attachment.fileSize + : 0, + publicUrl, + }) + } + const message = yield* db.transaction( + Effect.gen(function* () { + const [createdMessage] = yield* messageRepo.insert({ + channelId: link.hazelChannelId, + authorId, + content: payload.content, + embeds: null, + replyToMessageId, + threadChannelId: null, + deletedAt: null, + }) + + yield* outboxRepo.insert({ + eventType: "message_created", + aggregateId: createdMessage.id, + channelId: createdMessage.channelId, + payload: { + messageId: createdMessage.id, + channelId: createdMessage.channelId, + authorId: createdMessage.authorId, + content: createdMessage.content, + replyToMessageId: createdMessage.replyToMessageId, + }, + }) + + if (normalizedExternalAttachments.length > 0) { + const uploadedAtBase = Date.now() + yield* transactionAwareExecute((client) => + client.insert(schema.attachmentsTable).values( + normalizedExternalAttachments.map((attachment, index) => ({ + organizationId: connection.organizationId, + channelId: link.hazelChannelId, + messageId: createdMessage.id, + fileName: attachment.fileName, + fileSize: attachment.fileSize, + externalUrl: attachment.publicUrl, + uploadedBy: authorId, + status: "complete" as const, + uploadedAt: new Date(uploadedAtBase + index), + deletedAt: null, + })), + ), + ) + } + + yield* messageLinkRepo.insert({ + channelLinkId: link.id, + hazelMessageId: createdMessage.id, + externalMessageId: payload.externalMessageId, + source: "external", + rootHazelMessageId: null, + rootExternalMessageId: null, + hazelThreadChannelId: createdMessage.threadChannelId, + externalThreadId: payload.externalThreadId ?? null, + deletedAt: null, + }) + + return createdMessage }), ) - } - const link = linkOption.value - if (shouldIgnoreWebhookOrigin(connection.provider, link.settings, payload.externalWebhookId)) { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, channelLinkId: link.id, source: "external", dedupeKey, - status: "ignored", payload, }) - return { status: "ignored_webhook_origin" as const } - } + yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) - const existingMessageLink = yield* messageLinkRepo.findByExternalMessage( - link.id, - payload.externalMessageId, - ) + return { status: "created" as const, hazelMessageId: message.id } + }) - if (Option.isSome(existingMessageLink)) { - yield* writeReceipt({ + const ingestMessageUpdate = Effect.fn("discordSyncWorker.ingestMessageUpdate")(function* ( + payload: ChatSyncIngressMessageUpdate, + ) { + const dedupeKey = payload.dedupeKey ?? `external:message:update:${payload.externalMessageId}` + const claimed = yield* claimReceipt({ syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, source: "external", dedupeKey, - status: "ignored", }) - return { status: "already_linked" as const } - } + if (!claimed) { + return { status: "deduped" as const } + } - const authorId = payload.externalAuthorId - ? yield* resolveAuthorUserId({ - provider: connection.provider, - organizationId: connection.organizationId, - externalUserId: payload.externalAuthorId, - displayName: payload.externalAuthorDisplayName ?? "External User", - avatarUrl: payload.externalAuthorAvatarUrl ?? null, - }) - : (yield* integrationBotService.getOrCreateBotUser( - decodeProvider(connection.provider), - connection.organizationId, - )).id - const replyToMessageId = payload.externalReplyToMessageId - ? yield* resolveHazelMessageId({ - syncConnectionId: payload.syncConnectionId, - externalMessageId: payload.externalReplyToMessageId, - preferredChannelLinkId: link.id, - }).pipe( - Effect.map((id) => - Option.match(id, { - onNone: () => null, - onSome: (value) => value, - }), - ), - ) - : null - const normalizedExternalAttachments: Array = [] - for (const attachment of payload.externalAttachments ?? []) { - const fileName = attachment.fileName.trim() - const publicUrl = attachment.publicUrl.trim() - if (!fileName || !publicUrl) { - continue - } - normalizedExternalAttachments.push({ - externalAttachmentId: attachment.externalAttachmentId, - fileName, - fileSize: - Number.isFinite(attachment.fileSize) && attachment.fileSize >= 0 - ? attachment.fileSize - : 0, - publicUrl, - }) - } - const message = yield* db.transaction( - Effect.gen(function* () { - const [createdMessage] = yield* messageRepo.insert({ - channelId: link.hazelChannelId, - authorId, - content: payload.content, - embeds: null, - replyToMessageId, - threadChannelId: null, - deletedAt: null, - }) + const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) - yield* outboxRepo.insert({ - eventType: "message_created", - aggregateId: createdMessage.id, - channelId: createdMessage.channelId, - payload: { - messageId: createdMessage.id, - channelId: createdMessage.channelId, - authorId: createdMessage.authorId, - content: createdMessage.content, - replyToMessageId: createdMessage.replyToMessageId, - }, + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId: payload.syncConnectionId, + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + status: "ignored", + payload, }) + return { status: "ignored_connection_inactive" as const } + } + yield* getProviderAdapter(connection.provider) - if (normalizedExternalAttachments.length > 0) { - const uploadedAtBase = Date.now() - yield* transactionAwareExecute((client) => - client.insert(schema.attachmentsTable).values( - normalizedExternalAttachments.map((attachment, index) => ({ - organizationId: connection.organizationId, - channelId: link.hazelChannelId, - messageId: createdMessage.id, - fileName: attachment.fileName, - fileSize: attachment.fileSize, - externalUrl: attachment.publicUrl, - uploadedBy: authorId, - status: "complete" as const, - uploadedAt: new Date(uploadedAtBase + index), - deletedAt: null, - })), - ), - ) - } + const linkOption = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + payload.externalChannelId, + ) - yield* messageLinkRepo.insert({ + if (Option.isNone(linkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: payload.syncConnectionId, + externalChannelId: payload.externalChannelId, + }), + ) + } + const link = linkOption.value + if (shouldIgnoreWebhookOrigin(connection.provider, link.settings, payload.externalWebhookId)) { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, channelLinkId: link.id, - hazelMessageId: createdMessage.id, - externalMessageId: payload.externalMessageId, source: "external", - rootHazelMessageId: null, - rootExternalMessageId: null, - hazelThreadChannelId: createdMessage.threadChannelId, - externalThreadId: payload.externalThreadId ?? null, - deletedAt: null, + dedupeKey, + status: "ignored", + payload, }) + return { status: "ignored_webhook_origin" as const } + } - return createdMessage - }), - ) - - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - payload, - }) - yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) - - return { status: "created" as const, hazelMessageId: message.id } - }) - - const ingestMessageUpdate = Effect.fn("discordSyncWorker.ingestMessageUpdate")(function* ( - payload: ChatSyncIngressMessageUpdate, - ) { - const dedupeKey = payload.dedupeKey ?? `external:message:update:${payload.externalMessageId}` - const claimed = yield* claimReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - }) - if (!claimed) { - return { status: "deduped" as const } - } - - const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) - - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId: payload.syncConnectionId, - }), + const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( + link.id, + payload.externalMessageId, ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - status: "ignored", - payload, - }) - return { status: "ignored_connection_inactive" as const } - } - yield* getProviderAdapter(connection.provider) - const linkOption = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - payload.externalChannelId, - ) - - if (Option.isNone(linkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ + if (Option.isNone(messageLinkOption)) { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, - externalChannelId: payload.externalChannelId, + channelLinkId: link.id, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + + yield* db.transaction( + Effect.gen(function* () { + const updatedMessage = yield* messageRepo.update({ + id: messageLink.hazelMessageId, + content: payload.content, + updatedAt: new Date(), + }) + yield* outboxRepo.insert({ + eventType: "message_updated", + aggregateId: updatedMessage.id, + channelId: updatedMessage.channelId, + payload: { + messageId: updatedMessage.id, + }, + }) }), ) - } - const link = linkOption.value - if (shouldIgnoreWebhookOrigin(connection.provider, link.settings, payload.externalWebhookId)) { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, channelLinkId: link.id, source: "external", dedupeKey, - status: "ignored", payload, }) - return { status: "ignored_webhook_origin" as const } - } + yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) - const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( - link.id, - payload.externalMessageId, - ) + return { status: "updated" as const, hazelMessageId: messageLink.hazelMessageId } + }) - if (Option.isNone(messageLinkOption)) { - yield* writeReceipt({ + const ingestMessageDelete = Effect.fn("discordSyncWorker.ingestMessageDelete")(function* ( + payload: ChatSyncIngressMessageDelete, + ) { + const dedupeKey = payload.dedupeKey ?? `external:message:delete:${payload.externalMessageId}` + const claimed = yield* claimReceipt({ syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, source: "external", dedupeKey, - status: "ignored", - payload, }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - - yield* db.transaction( - Effect.gen(function* () { - const updatedMessage = yield* messageRepo.update({ - id: messageLink.hazelMessageId, - content: payload.content, - updatedAt: new Date(), - }) - yield* outboxRepo.insert({ - eventType: "message_updated", - aggregateId: updatedMessage.id, - channelId: updatedMessage.channelId, - payload: { - messageId: updatedMessage.id, - }, - }) - }), - ) + if (!claimed) { + return { status: "deduped" as const } + } - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - payload, - }) - yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) - - return { status: "updated" as const, hazelMessageId: messageLink.hazelMessageId } - }) - - const ingestMessageDelete = Effect.fn("discordSyncWorker.ingestMessageDelete")(function* ( - payload: ChatSyncIngressMessageDelete, - ) { - const dedupeKey = payload.dedupeKey ?? `external:message:delete:${payload.externalMessageId}` - const claimed = yield* claimReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - }) - if (!claimed) { - return { status: "deduped" as const } - } + const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) - const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId: payload.syncConnectionId, + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "ignored_connection_inactive" as const } + } + yield* getProviderAdapter(connection.provider) + + const linkOption = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + payload.externalChannelId, + ) - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ + if (Option.isNone(linkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: payload.syncConnectionId, + externalChannelId: payload.externalChannelId, + }), + ) + } + const link = linkOption.value + if (shouldIgnoreWebhookOrigin(connection.provider, link.settings, payload.externalWebhookId)) { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, - }), + channelLinkId: link.id, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "ignored_webhook_origin" as const } + } + + const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( + link.id, + payload.externalMessageId, ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - status: "ignored", - payload, - }) - return { status: "ignored_connection_inactive" as const } - } - yield* getProviderAdapter(connection.provider) - const linkOption = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - payload.externalChannelId, - ) + if (Option.isNone(messageLinkOption)) { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value - if (Option.isNone(linkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: payload.syncConnectionId, - externalChannelId: payload.externalChannelId, + yield* db.transaction( + Effect.gen(function* () { + const deletedMessage = yield* messageRepo.update({ + id: messageLink.hazelMessageId, + deletedAt: new Date(), + updatedAt: new Date(), + }) + yield* outboxRepo.insert({ + eventType: "message_deleted", + aggregateId: deletedMessage.id, + channelId: deletedMessage.channelId, + payload: { + messageId: deletedMessage.id, + channelId: deletedMessage.channelId, + }, + }) }), ) - } - const link = linkOption.value - if (shouldIgnoreWebhookOrigin(connection.provider, link.settings, payload.externalWebhookId)) { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, channelLinkId: link.id, source: "external", dedupeKey, - status: "ignored", payload, }) - return { status: "ignored_webhook_origin" as const } - } + yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) - const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( - link.id, - payload.externalMessageId, - ) + return { status: "deleted" as const, hazelMessageId: messageLink.hazelMessageId } + }) - if (Option.isNone(messageLinkOption)) { - yield* writeReceipt({ + const ingestReactionAdd = Effect.fn("discordSyncWorker.ingestReactionAdd")(function* ( + payload: ChatSyncIngressReactionAdd, + ) { + const dedupeKey = + payload.dedupeKey ?? + `external:reaction:add:${payload.externalMessageId}:${payload.externalUserId}:${payload.emoji}` + const claimed = yield* claimReceipt({ syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, source: "external", dedupeKey, - status: "ignored", - payload, }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - - yield* db.transaction( - Effect.gen(function* () { - const deletedMessage = yield* messageRepo.update({ - id: messageLink.hazelMessageId, - deletedAt: new Date(), - updatedAt: new Date(), - }) - yield* outboxRepo.insert({ - eventType: "message_deleted", - aggregateId: deletedMessage.id, - channelId: deletedMessage.channelId, - payload: { - messageId: deletedMessage.id, - channelId: deletedMessage.channelId, - }, + if (!claimed) { + return { status: "deduped" as const } + } + + const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId: payload.syncConnectionId, + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + status: "ignored", + payload, }) - }), - ) + return { status: "ignored_connection_inactive" as const } + } + yield* getProviderAdapter(connection.provider) - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - payload, - }) - yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) - - return { status: "deleted" as const, hazelMessageId: messageLink.hazelMessageId } - }) - - const ingestReactionAdd = Effect.fn("discordSyncWorker.ingestReactionAdd")(function* ( - payload: ChatSyncIngressReactionAdd, - ) { - const dedupeKey = - payload.dedupeKey ?? - `external:reaction:add:${payload.externalMessageId}:${payload.externalUserId}:${payload.emoji}` - const claimed = yield* claimReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - }) - if (!claimed) { - return { status: "deduped" as const } - } + const linkOption = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + payload.externalChannelId, + ) - const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + if (Option.isNone(linkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: payload.syncConnectionId, + externalChannelId: payload.externalChannelId, + }), + ) + } + const link = linkOption.value - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId: payload.syncConnectionId, - }), + const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( + link.id, + payload.externalMessageId, ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - status: "ignored", - payload, + + if (Option.isNone(messageLinkOption)) { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + + const userId = yield* resolveAuthorUserId({ + provider: connection.provider, + organizationId: connection.organizationId, + externalUserId: payload.externalUserId, + displayName: payload.externalAuthorDisplayName ?? "Discord User", + avatarUrl: payload.externalAuthorAvatarUrl ?? null, }) - return { status: "ignored_connection_inactive" as const } - } - yield* getProviderAdapter(connection.provider) - const linkOption = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - payload.externalChannelId, - ) + const existingReaction = yield* messageReactionRepo.findByMessageUserEmoji( + messageLink.hazelMessageId, + userId, + payload.emoji, + ) - if (Option.isNone(linkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ + if (Option.isSome(existingReaction)) { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, - externalChannelId: payload.externalChannelId, + channelLinkId: link.id, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "already_exists" as const } + } + + const reaction = yield* db.transaction( + Effect.gen(function* () { + const [createdReaction] = yield* messageReactionRepo.insert({ + messageId: messageLink.hazelMessageId, + channelId: link.hazelChannelId, + userId, + emoji: payload.emoji, + }) + yield* outboxRepo.insert({ + eventType: "reaction_created", + aggregateId: createdReaction.id, + channelId: createdReaction.channelId, + payload: { + reactionId: createdReaction.id, + }, + }) + return createdReaction }), ) - } - const link = linkOption.value - const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( - link.id, - payload.externalMessageId, - ) - - if (Option.isNone(messageLinkOption)) { yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, channelLinkId: link.id, source: "external", dedupeKey, - status: "ignored", payload, }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - - const userId = yield* resolveAuthorUserId({ - provider: connection.provider, - organizationId: connection.organizationId, - externalUserId: payload.externalUserId, - displayName: payload.externalAuthorDisplayName ?? "Discord User", - avatarUrl: payload.externalAuthorAvatarUrl ?? null, - }) + yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) - const existingReaction = yield* messageReactionRepo.findByMessageUserEmoji( - messageLink.hazelMessageId, - userId, - payload.emoji, - ) + return { status: "created" as const, hazelReactionId: reaction.id } + }) - if (Option.isSome(existingReaction)) { - yield* writeReceipt({ + const ingestReactionRemove = Effect.fn("discordSyncWorker.ingestReactionRemove")(function* ( + payload: ChatSyncIngressReactionRemove, + ) { + const dedupeKey = + payload.dedupeKey ?? + `external:reaction:remove:${payload.externalMessageId}:${payload.externalUserId}:${payload.emoji}` + const claimed = yield* claimReceipt({ syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, source: "external", dedupeKey, - status: "ignored", - payload, }) - return { status: "already_exists" as const } - } + if (!claimed) { + return { status: "deduped" as const } + } - const reaction = yield* db.transaction( - Effect.gen(function* () { - const [createdReaction] = yield* messageReactionRepo.insert({ - messageId: messageLink.hazelMessageId, - channelId: link.hazelChannelId, - userId, - emoji: payload.emoji, - }) - yield* outboxRepo.insert({ - eventType: "reaction_created", - aggregateId: createdReaction.id, - channelId: createdReaction.channelId, - payload: { - reactionId: createdReaction.id, - }, + const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId: payload.syncConnectionId, + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + status: "ignored", + payload, }) - return createdReaction - }), - ) + return { status: "ignored_connection_inactive" as const } + } + yield* getProviderAdapter(connection.provider) - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - payload, - }) - yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) - - return { status: "created" as const, hazelReactionId: reaction.id } - }) - - const ingestReactionRemove = Effect.fn("discordSyncWorker.ingestReactionRemove")(function* ( - payload: ChatSyncIngressReactionRemove, - ) { - const dedupeKey = - payload.dedupeKey ?? - `external:reaction:remove:${payload.externalMessageId}:${payload.externalUserId}:${payload.emoji}` - const claimed = yield* claimReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - }) - if (!claimed) { - return { status: "deduped" as const } - } + const linkOption = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + payload.externalChannelId, + ) - const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + if (Option.isNone(linkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: payload.syncConnectionId, + externalChannelId: payload.externalChannelId, + }), + ) + } + const link = linkOption.value - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId: payload.syncConnectionId, - }), + const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( + link.id, + payload.externalMessageId, ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - status: "ignored", - payload, + + if (Option.isNone(messageLinkOption)) { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: link.id, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "ignored_missing_link" as const } + } + const messageLink = messageLinkOption.value + + const userId = yield* resolveAuthorUserId({ + provider: connection.provider, + organizationId: connection.organizationId, + externalUserId: payload.externalUserId, + displayName: payload.externalAuthorDisplayName ?? "Discord User", + avatarUrl: payload.externalAuthorAvatarUrl ?? null, }) - return { status: "ignored_connection_inactive" as const } - } - yield* getProviderAdapter(connection.provider) - const linkOption = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - payload.externalChannelId, - ) + const existingReaction = yield* messageReactionRepo.findByMessageUserEmoji( + messageLink.hazelMessageId, + userId, + payload.emoji, + ) - if (Option.isNone(linkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ + if (Option.isNone(existingReaction)) { + yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, - externalChannelId: payload.externalChannelId, + channelLinkId: link.id, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "already_deleted" as const } + } + + yield* db.transaction( + Effect.gen(function* () { + yield* messageReactionRepo.deleteById(existingReaction.value.id) + yield* outboxRepo.insert({ + eventType: "reaction_deleted", + aggregateId: existingReaction.value.id, + channelId: link.hazelChannelId, + payload: { + hazelChannelId: link.hazelChannelId, + hazelMessageId: messageLink.hazelMessageId, + emoji: payload.emoji, + userId, + }, + }) }), ) - } - const link = linkOption.value - - const messageLinkOption = yield* messageLinkRepo.findByExternalMessage( - link.id, - payload.externalMessageId, - ) - if (Option.isNone(messageLinkOption)) { yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, channelLinkId: link.id, source: "external", dedupeKey, - status: "ignored", payload, }) - return { status: "ignored_missing_link" as const } - } - const messageLink = messageLinkOption.value - - const userId = yield* resolveAuthorUserId({ - provider: connection.provider, - organizationId: connection.organizationId, - externalUserId: payload.externalUserId, - displayName: payload.externalAuthorDisplayName ?? "Discord User", - avatarUrl: payload.externalAuthorAvatarUrl ?? null, - }) + yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(link.id) - const existingReaction = yield* messageReactionRepo.findByMessageUserEmoji( - messageLink.hazelMessageId, - userId, - payload.emoji, - ) + return { status: "deleted" as const, hazelReactionId: existingReaction.value.id } + }) - if (Option.isNone(existingReaction)) { - yield* writeReceipt({ + const ingestThreadCreate = Effect.fn("discordSyncWorker.ingestThreadCreate")(function* ( + payload: ChatSyncIngressThreadCreate, + ) { + const dedupeKey = payload.dedupeKey ?? `external:thread:create:${payload.externalThreadId}` + const claimed = yield* claimReceipt({ syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, source: "external", dedupeKey, - status: "ignored", - payload, }) - return { status: "already_deleted" as const } - } + if (!claimed) { + return { status: "deduped" as const } + } - yield* db.transaction( - Effect.gen(function* () { - yield* messageReactionRepo.deleteById(existingReaction.value.id) - yield* outboxRepo.insert({ - eventType: "reaction_deleted", - aggregateId: existingReaction.value.id, - channelId: link.hazelChannelId, - payload: { - hazelChannelId: link.hazelChannelId, - hazelMessageId: messageLink.hazelMessageId, - emoji: payload.emoji, - userId, - }, + const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + + if (Option.isNone(connectionOption)) { + return yield* Effect.fail( + new DiscordSyncConnectionNotFoundError({ + syncConnectionId: payload.syncConnectionId, + }), + ) + } + const connection = connectionOption.value + if (connection.status !== "active") { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + source: "external", + dedupeKey, + status: "ignored", + payload, }) - }), - ) + return { status: "ignored_connection_inactive" as const } + } + yield* getProviderAdapter(connection.provider) - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - channelLinkId: link.id, - source: "external", - dedupeKey, - payload, - }) - yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(link.id) - - return { status: "deleted" as const, hazelReactionId: existingReaction.value.id } - }) - - const ingestThreadCreate = Effect.fn("discordSyncWorker.ingestThreadCreate")(function* ( - payload: ChatSyncIngressThreadCreate, - ) { - const dedupeKey = payload.dedupeKey ?? `external:thread:create:${payload.externalThreadId}` - const claimed = yield* claimReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - }) - if (!claimed) { - return { status: "deduped" as const } - } + const parentLinkOption = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + payload.externalParentChannelId, + ) - const connectionOption = yield* connectionRepo.findById(payload.syncConnectionId) + if (Option.isNone(parentLinkOption)) { + return yield* Effect.fail( + new DiscordSyncChannelLinkNotFoundError({ + syncConnectionId: payload.syncConnectionId, + externalChannelId: payload.externalParentChannelId, + }), + ) + } + const parentLink = parentLinkOption.value + const externalThreadChannelId = payload.externalThreadId as unknown as ExternalChannelId - if (Option.isNone(connectionOption)) { - return yield* Effect.fail( - new DiscordSyncConnectionNotFoundError({ - syncConnectionId: payload.syncConnectionId, - }), + const existingThreadLink = yield* channelLinkRepo.findByExternalChannel( + payload.syncConnectionId, + externalThreadChannelId, ) - } - const connection = connectionOption.value - if (connection.status !== "active") { - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - source: "external", - dedupeKey, - status: "ignored", - payload, + + if (Option.isSome(existingThreadLink)) { + yield* writeReceipt({ + syncConnectionId: payload.syncConnectionId, + channelLinkId: existingThreadLink.value.id, + source: "external", + dedupeKey, + status: "ignored", + payload, + }) + return { status: "already_linked" as const } + } + + const [threadChannel] = yield* channelRepo.insert({ + name: payload.name?.trim() || "Thread", + icon: null, + type: "thread", + organizationId: connection.organizationId, + parentChannelId: parentLink.hazelChannelId, + sectionId: null, + deletedAt: null, }) - return { status: "ignored_connection_inactive" as const } - } - yield* getProviderAdapter(connection.provider) - const parentLinkOption = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - payload.externalParentChannelId, - ) + yield* channelAccessSyncService.syncChannel(threadChannel.id) - if (Option.isNone(parentLinkOption)) { - return yield* Effect.fail( - new DiscordSyncChannelLinkNotFoundError({ - syncConnectionId: payload.syncConnectionId, - externalChannelId: payload.externalParentChannelId, - }), - ) - } - const parentLink = parentLinkOption.value - const externalThreadChannelId = payload.externalThreadId as unknown as ExternalChannelId + const [threadLink] = yield* channelLinkRepo.insert({ + syncConnectionId: payload.syncConnectionId, + hazelChannelId: threadChannel.id, + externalChannelId: externalThreadChannelId, + externalChannelName: payload.name ?? null, + direction: parentLink.direction, + isActive: true, + settings: parentLink.settings, + lastSyncedAt: null, + deletedAt: null, + }) - const existingThreadLink = yield* channelLinkRepo.findByExternalChannel( - payload.syncConnectionId, - externalThreadChannelId, - ) + if (payload.externalRootMessageId) { + const rootMessageLink = yield* messageLinkRepo.findByExternalMessage( + parentLink.id, + payload.externalRootMessageId, + ) + + if (Option.isSome(rootMessageLink)) { + yield* db.transaction( + Effect.gen(function* () { + const updatedMessage = yield* messageRepo.update({ + id: rootMessageLink.value.hazelMessageId, + threadChannelId: threadChannel.id, + updatedAt: new Date(), + }) + yield* outboxRepo.insert({ + eventType: "message_updated", + aggregateId: updatedMessage.id, + channelId: updatedMessage.channelId, + payload: { + messageId: updatedMessage.id, + }, + }) + }), + ) + } + } - if (Option.isSome(existingThreadLink)) { yield* writeReceipt({ syncConnectionId: payload.syncConnectionId, - channelLinkId: existingThreadLink.value.id, + channelLinkId: threadLink.id, source: "external", dedupeKey, - status: "ignored", payload, }) - return { status: "already_linked" as const } - } + yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) + yield* channelLinkRepo.updateLastSyncedAt(threadLink.id) + yield* channelLinkRepo.updateLastSyncedAt(parentLink.id) - const [threadChannel] = yield* channelRepo.insert({ - name: payload.name?.trim() || "Thread", - icon: null, - type: "thread", - organizationId: connection.organizationId, - parentChannelId: parentLink.hazelChannelId, - sectionId: null, - deletedAt: null, - }) - - yield* channelAccessSyncService.syncChannel(threadChannel.id) - - const [threadLink] = yield* channelLinkRepo.insert({ - syncConnectionId: payload.syncConnectionId, - hazelChannelId: threadChannel.id, - externalChannelId: externalThreadChannelId, - externalChannelName: payload.name ?? null, - direction: parentLink.direction, - isActive: true, - settings: parentLink.settings, - lastSyncedAt: null, - deletedAt: null, - }) - - if (payload.externalRootMessageId) { - const rootMessageLink = yield* messageLinkRepo.findByExternalMessage( - parentLink.id, - payload.externalRootMessageId, - ) - - if (Option.isSome(rootMessageLink)) { - yield* db.transaction( - Effect.gen(function* () { - const updatedMessage = yield* messageRepo.update({ - id: rootMessageLink.value.hazelMessageId, - threadChannelId: threadChannel.id, - updatedAt: new Date(), - }) - yield* outboxRepo.insert({ - eventType: "message_updated", - aggregateId: updatedMessage.id, - channelId: updatedMessage.channelId, - payload: { - messageId: updatedMessage.id, - }, - }) - }), - ) + return { + status: "created" as const, + hazelThreadChannelId: threadChannel.id, + channelLinkId: threadLink.id, } - } - - yield* writeReceipt({ - syncConnectionId: payload.syncConnectionId, - channelLinkId: threadLink.id, - source: "external", - dedupeKey, - payload, }) - yield* connectionRepo.updateLastSyncedAt(payload.syncConnectionId) - yield* channelLinkRepo.updateLastSyncedAt(threadLink.id) - yield* channelLinkRepo.updateLastSyncedAt(parentLink.id) return { - status: "created" as const, - hazelThreadChannelId: threadChannel.id, - channelLinkId: threadLink.id, + syncConnection, + syncAllActiveConnections, + syncHazelMessageToProvider, + syncHazelMessageUpdateToProvider, + syncHazelMessageDeleteToProvider, + syncHazelReactionCreateToProvider, + syncHazelReactionDeleteToProvider, + syncHazelMessageCreateToAllConnections, + syncHazelMessageUpdateToAllConnections, + syncHazelMessageDeleteToAllConnections, + syncHazelReactionCreateToAllConnections, + syncHazelReactionDeleteToAllConnections, + ingestMessageCreate, + ingestMessageUpdate, + ingestMessageDelete, + ingestReactionAdd, + ingestReactionRemove, + ingestThreadCreate, } - }) - - return { - syncConnection, - syncAllActiveConnections, - syncHazelMessageToProvider, - syncHazelMessageUpdateToProvider, - syncHazelMessageDeleteToProvider, - syncHazelReactionCreateToProvider, - syncHazelReactionDeleteToProvider, - syncHazelMessageCreateToAllConnections, - syncHazelMessageUpdateToAllConnections, - syncHazelMessageDeleteToAllConnections, - syncHazelReactionCreateToAllConnections, - syncHazelReactionDeleteToAllConnections, - ingestMessageCreate, - ingestMessageUpdate, - ingestMessageDelete, - ingestReactionAdd, - ingestReactionRemove, - ingestThreadCreate, - } -}) + }, +) export const ChatSyncCoreWorkerLayer = Layer.effect(ChatSyncCoreWorker, ChatSyncCoreWorkerMake).pipe( Layer.provide(ChatSyncConnectionRepo.layer), diff --git a/apps/backend/src/services/chat-sync/discord-gateway-service.dispatch.test.ts b/apps/backend/src/services/chat-sync/discord-gateway-service.dispatch.test.ts index 396ef5fdc..de3ab67b6 100644 --- a/apps/backend/src/services/chat-sync/discord-gateway-service.dispatch.test.ts +++ b/apps/backend/src/services/chat-sync/discord-gateway-service.dispatch.test.ts @@ -50,30 +50,30 @@ const makeDispatchHarness = () => { } const discordSyncWorker: DispatchWorker = { - ingestMessageCreate: (payload) => - Effect.sync(() => { - calls.create.push(payload) - }), - ingestMessageUpdate: (payload) => - Effect.sync(() => { - calls.update.push(payload) - }), - ingestMessageDelete: (payload) => - Effect.sync(() => { - calls.delete.push(payload) - }), - ingestReactionAdd: (payload) => - Effect.sync(() => { - calls.reactionAdd.push(payload) - }), - ingestReactionRemove: (payload) => - Effect.sync(() => { - calls.reactionRemove.push(payload) - }), - ingestThreadCreate: (payload) => - Effect.sync(() => { - calls.threadCreate.push(payload) - }), + ingestMessageCreate: (payload) => + Effect.sync(() => { + calls.create.push(payload) + }), + ingestMessageUpdate: (payload) => + Effect.sync(() => { + calls.update.push(payload) + }), + ingestMessageDelete: (payload) => + Effect.sync(() => { + calls.delete.push(payload) + }), + ingestReactionAdd: (payload) => + Effect.sync(() => { + calls.reactionAdd.push(payload) + }), + ingestReactionRemove: (payload) => + Effect.sync(() => { + calls.reactionRemove.push(payload) + }), + ingestThreadCreate: (payload) => + Effect.sync(() => { + calls.threadCreate.push(payload) + }), } const handlers = createDiscordGatewayDispatchHandlers({ diff --git a/apps/backend/src/services/chat-sync/discord-gateway-shared.ts b/apps/backend/src/services/chat-sync/discord-gateway-shared.ts index 4722ac8e9..90fc65fb5 100644 --- a/apps/backend/src/services/chat-sync/discord-gateway-shared.ts +++ b/apps/backend/src/services/chat-sync/discord-gateway-shared.ts @@ -1,5 +1,12 @@ import { createHash } from "node:crypto" -import { ExternalChannelId, ExternalMessageId, ExternalThreadId, ExternalUserId, ExternalWebhookId, SyncConnectionId } from "@hazel/schema" +import { + ExternalChannelId, + ExternalMessageId, + ExternalThreadId, + ExternalUserId, + ExternalWebhookId, + SyncConnectionId, +} from "@hazel/schema" import { ServiceMap, Effect, Option, Schema } from "effect" import { DiscordSyncWorker } from "./discord-sync-worker" import type { ChatSyncIngressMessageAttachment } from "./chat-sync-core-worker" @@ -164,15 +171,10 @@ const decodeExternalUserId: ExternalIdDecoder = (value) => const decodeExternalWebhookId: ExternalIdDecoder = (value) => Schema.decodeUnknownOption(ExternalWebhookId)(value) -export const decodeRequiredExternalId = ( - value: unknown, - decode: ExternalIdDecoder, -): Option.Option => decode(value) +export const decodeRequiredExternalId = (value: unknown, decode: ExternalIdDecoder): Option.Option => + decode(value) -export const decodeOptionalExternalId = ( - value: unknown, - decode: ExternalIdDecoder, -): A | undefined => { +export const decodeOptionalExternalId = (value: unknown, decode: ExternalIdDecoder): A | undefined => { if (value === undefined) return undefined const decoded = decode(value) return Option.isSome(decoded) ? decoded.value : undefined @@ -346,16 +348,16 @@ export const createDiscordGatewayDispatchHandlers = (deps: { .slice(0, 16) yield* Effect.forEach(inboundLinks, (link) => - deps.discordSyncWorker.ingestMessageUpdate({ - syncConnectionId: link.syncConnectionId, - externalChannelId: externalChannelIdOption.value, - externalMessageId: externalMessageIdOption.value, - externalWebhookId, - content, - dedupeKey: `discord:gateway:update:${externalMessageIdOption.value}:${dedupeSuffix}`, - }), - ) - }) + deps.discordSyncWorker.ingestMessageUpdate({ + syncConnectionId: link.syncConnectionId, + externalChannelId: externalChannelIdOption.value, + externalMessageId: externalMessageIdOption.value, + externalWebhookId, + content, + dedupeKey: `discord:gateway:update:${externalMessageIdOption.value}:${dedupeSuffix}`, + }), + ) + }) const ingestMessageDeleteEvent = Effect.fn("DiscordGatewayService.ingestMessageDeleteEvent")(function* ( event: DiscordMessageDeleteEvent, @@ -399,55 +401,55 @@ export const createDiscordGatewayDispatchHandlers = (deps: { ) }) - const ingestMessageReactionAddEvent = Effect.fn( - "DiscordGatewayService.ingestMessageReactionAddEvent", - )(function* (event: DiscordMessageReactionAddEvent) { - if (!event.channel_id || !event.message_id || !event.user_id) return + const ingestMessageReactionAddEvent = Effect.fn("DiscordGatewayService.ingestMessageReactionAddEvent")( + function* (event: DiscordMessageReactionAddEvent) { + if (!event.channel_id || !event.message_id || !event.user_id) return - const emoji = formatDiscordEmoji(event.emoji) - if (!emoji) return + const emoji = formatDiscordEmoji(event.emoji) + if (!emoji) return - const { externalAuthorDisplayName, externalAuthorAvatarUrl } = extractReactionAuthor(event) - const externalChannelIdOption = yield* decodeRequiredExternalIdOrWarn({ - eventType: "MESSAGE_REACTION_ADD", - field: "channel_id", - value: event.channel_id, - decode: decodeExternalChannelId, - }) - if (Option.isNone(externalChannelIdOption)) return + const { externalAuthorDisplayName, externalAuthorAvatarUrl } = extractReactionAuthor(event) + const externalChannelIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_REACTION_ADD", + field: "channel_id", + value: event.channel_id, + decode: decodeExternalChannelId, + }) + if (Option.isNone(externalChannelIdOption)) return - const externalMessageIdOption = yield* decodeRequiredExternalIdOrWarn({ - eventType: "MESSAGE_REACTION_ADD", - field: "message_id", - value: event.message_id, - decode: decodeExternalMessageId, - }) - if (Option.isNone(externalMessageIdOption)) return + const externalMessageIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_REACTION_ADD", + field: "message_id", + value: event.message_id, + decode: decodeExternalMessageId, + }) + if (Option.isNone(externalMessageIdOption)) return - const externalUserIdOption = yield* decodeRequiredExternalIdOrWarn({ - eventType: "MESSAGE_REACTION_ADD", - field: "user_id", - value: event.user_id, - decode: decodeExternalUserId, - }) - if (Option.isNone(externalUserIdOption)) return + const externalUserIdOption = yield* decodeRequiredExternalIdOrWarn({ + eventType: "MESSAGE_REACTION_ADD", + field: "user_id", + value: event.user_id, + decode: decodeExternalUserId, + }) + if (Option.isNone(externalUserIdOption)) return - const links = yield* deps.findActiveLinksByExternalChannel(externalChannelIdOption.value) - const inboundLinks = links.filter((link) => link.direction !== "hazel_to_external") + const links = yield* deps.findActiveLinksByExternalChannel(externalChannelIdOption.value) + const inboundLinks = links.filter((link) => link.direction !== "hazel_to_external") - yield* Effect.forEach(inboundLinks, (link) => - deps.discordSyncWorker.ingestReactionAdd({ - syncConnectionId: link.syncConnectionId, - externalChannelId: externalChannelIdOption.value, - externalMessageId: externalMessageIdOption.value, - externalUserId: externalUserIdOption.value, - emoji, - externalAuthorDisplayName, - externalAuthorAvatarUrl, - dedupeKey: `discord:gateway:reaction:add:${externalChannelIdOption.value}:${externalMessageIdOption.value}:${externalUserIdOption.value}:${emoji}`, - }), - ) - }) + yield* Effect.forEach(inboundLinks, (link) => + deps.discordSyncWorker.ingestReactionAdd({ + syncConnectionId: link.syncConnectionId, + externalChannelId: externalChannelIdOption.value, + externalMessageId: externalMessageIdOption.value, + externalUserId: externalUserIdOption.value, + emoji, + externalAuthorDisplayName, + externalAuthorAvatarUrl, + dedupeKey: `discord:gateway:reaction:add:${externalChannelIdOption.value}:${externalMessageIdOption.value}:${externalUserIdOption.value}:${emoji}`, + }), + ) + }, + ) const ingestMessageReactionRemoveEvent = Effect.fn( "DiscordGatewayService.ingestMessageReactionRemoveEvent", diff --git a/apps/backend/src/services/chat-sync/discord-sync-worker.test.ts b/apps/backend/src/services/chat-sync/discord-sync-worker.test.ts index f17dad5cb..0f036ecdf 100644 --- a/apps/backend/src/services/chat-sync/discord-sync-worker.test.ts +++ b/apps/backend/src/services/chat-sync/discord-sync-worker.test.ts @@ -235,9 +235,11 @@ const makeWorkerLayer = (deps: WorkerLayerDeps) => Layer.provide( Layer.succeed( MessageOutboxRepo, - serviceShape(deps.messageOutboxRepo ?? { - insert: () => Effect.succeed([{ id: "outbox-id" }]), - }), + serviceShape( + deps.messageOutboxRepo ?? { + insert: () => Effect.succeed([{ id: "outbox-id" }]), + }, + ), ), ), Layer.provide( @@ -279,9 +281,8 @@ const makeWorkerLayerWithOverrides = (deps: WorkerLayerDeps) => { return layer } -const useWorker = ( - fn: (worker: DiscordSyncWorkerShape) => Effect.Effect, -) => DiscordSyncWorkerTag.use(fn) +const useWorker = (fn: (worker: DiscordSyncWorkerShape) => Effect.Effect) => + DiscordSyncWorkerTag.use(fn) const DiscordSyncWorker = { ingestMessageCreate: (payload: DiscordIngressMessageCreate) => @@ -308,11 +309,7 @@ const DiscordSyncWorker = { dedupeKeyOverride?: string, ) => useWorker((worker) => - worker.syncHazelMessageUpdateToDiscord( - syncConnectionId, - hazelMessageId, - dedupeKeyOverride, - ), + worker.syncHazelMessageUpdateToDiscord(syncConnectionId, hazelMessageId, dedupeKeyOverride), ), syncHazelMessageDeleteToDiscord: ( syncConnectionId: SyncConnectionId, @@ -320,11 +317,7 @@ const DiscordSyncWorker = { dedupeKeyOverride?: string, ) => useWorker((worker) => - worker.syncHazelMessageDeleteToDiscord( - syncConnectionId, - hazelMessageId, - dedupeKeyOverride, - ), + worker.syncHazelMessageDeleteToDiscord(syncConnectionId, hazelMessageId, dedupeKeyOverride), ), } @@ -1441,9 +1434,7 @@ describe("DiscordSyncWorker outbound webhook dispatch", () => { externalMessageId: ExternalMessageId }) => { insertedExternalMessageId = payload.externalMessageId - return Effect.succeed([ - { id: "message-link-id", channelLinkId: payload.channelLinkId }, - ]) + return Effect.succeed([{ id: "message-link-id", channelLinkId: payload.channelLinkId }]) }, } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { @@ -3015,8 +3006,7 @@ describe("DiscordSyncWorker outbound attachments primitive", () => { } as unknown as ChatSyncChannelLinkRepo, messageLinkRepo: { findByHazelMessage: () => Effect.succeed(Option.none()), - insert: () => - Effect.succeed([{ id: "message-link-id", channelLinkId: CHANNEL_LINK_ID }]), + insert: () => Effect.succeed([{ id: "message-link-id", channelLinkId: CHANNEL_LINK_ID }]), } as unknown as ChatSyncMessageLinkRepo, eventReceiptRepo: { claimByDedupeKey: () => Effect.succeed(true), @@ -3199,9 +3189,7 @@ describe("DiscordSyncWorker outbound attachments primitive", () => { expect(result.status).toBe("synced") expect(createMessageWithAttachmentsCalled).toBe(true) - expect(firstAttachmentUrl).toBe( - `${ATTACHMENT_PUBLIC_URL}/00000000-0000-4000-8000-000000000201`, - ) + expect(firstAttachmentUrl).toBe(`${ATTACHMENT_PUBLIC_URL}/00000000-0000-4000-8000-000000000201`) }) it("fails with configuration error when attachments exist and S3_PUBLIC_URL is missing", async () => { diff --git a/apps/backend/src/services/connect-conversation-service.test.ts b/apps/backend/src/services/connect-conversation-service.test.ts index a07f46d8e..4b85b47df 100644 --- a/apps/backend/src/services/connect-conversation-service.test.ts +++ b/apps/backend/src/services/connect-conversation-service.test.ts @@ -76,87 +76,113 @@ type ConnectParticipantUpsertInput = Parameters< >[0] const makeChannelRepoLayer = () => - Layer.succeed(ChannelRepo, serviceShape({ - findById: () => Effect.succeed(Option.none()), - })) + Layer.succeed( + ChannelRepo, + serviceShape({ + findById: () => Effect.succeed(Option.none()), + }), + ) const makeMessageRepoLayer = () => - Layer.succeed(MessageRepo, serviceShape({ - backfillConversationIdForChannel: () => Effect.succeed(undefined), - })) + Layer.succeed( + MessageRepo, + serviceShape({ + backfillConversationIdForChannel: () => Effect.succeed(undefined), + }), + ) const makeMessageReactionRepoLayer = () => - Layer.succeed(MessageReactionRepo, serviceShape({ - backfillConversationIdForChannel: () => Effect.succeed(undefined), - })) + Layer.succeed( + MessageReactionRepo, + serviceShape({ + backfillConversationIdForChannel: () => Effect.succeed(undefined), + }), + ) const makeOrgResolverLayer = () => - Layer.succeed(OrgResolver, serviceShape({ - fromChannelWithAccess: () => Effect.succeed(undefined), - })) + Layer.succeed( + OrgResolver, + serviceShape({ + fromChannelWithAccess: () => Effect.succeed(undefined), + }), + ) const makeConversationRepoLayer = (conversation: MutableConversation) => - Layer.succeed(ConnectConversationRepo, serviceShape({ - findById: (id: ConnectConversationId) => - Effect.succeed(id === conversation.id ? Option.some(conversation) : Option.none()), - update: (patch: Partial & { id: ConnectConversationId }) => - Effect.sync(() => { - Object.assign(conversation, patch) - return conversation - }), - insert: () => Effect.die("not implemented"), - })) + Layer.succeed( + ConnectConversationRepo, + serviceShape({ + findById: (id: ConnectConversationId) => + Effect.succeed(id === conversation.id ? Option.some(conversation) : Option.none()), + update: (patch: Partial & { id: ConnectConversationId }) => + Effect.sync(() => { + Object.assign(conversation, patch) + return conversation + }), + insert: () => Effect.die("not implemented"), + }), + ) const makeMountRepoLayer = (mounts: MutableMount[]) => - Layer.succeed(ConnectConversationChannelRepo, serviceShape({ - findByChannelId: (channelId: ChannelId) => - Effect.succeed( - Option.fromNullishOr( - mounts.find((mount) => mount.channelId === channelId && mount.deletedAt === null), + Layer.succeed( + ConnectConversationChannelRepo, + serviceShape({ + findByChannelId: (channelId: ChannelId) => + Effect.succeed( + Option.fromNullishOr( + mounts.find((mount) => mount.channelId === channelId && mount.deletedAt === null), + ), ), - ), - findByConversationId: (conversationId: ConnectConversationId) => - Effect.succeed( - mounts.filter((mount) => mount.conversationId === conversationId && mount.deletedAt === null), - ), - update: (patch: Partial & { id: ConnectConversationChannelId }) => - Effect.sync(() => { - const mount = mounts.find((candidate) => candidate.id === patch.id) - if (!mount) throw new Error(`Missing mount ${patch.id}`) - Object.assign(mount, patch) - return mount - }), - insert: () => Effect.die("not implemented"), - })) + findByConversationId: (conversationId: ConnectConversationId) => + Effect.succeed( + mounts.filter( + (mount) => mount.conversationId === conversationId && mount.deletedAt === null, + ), + ), + update: (patch: Partial & { id: ConnectConversationChannelId }) => + Effect.sync(() => { + const mount = mounts.find((candidate) => candidate.id === patch.id) + if (!mount) throw new Error(`Missing mount ${patch.id}`) + Object.assign(mount, patch) + return mount + }), + insert: () => Effect.die("not implemented"), + }), + ) const makeParticipantRepoLayer = (participants: MutableParticipant[]) => - Layer.succeed(ConnectParticipantRepo, serviceShape({ - listByConversation: (conversationId: ConnectConversationId) => - Effect.succeed( - participants.filter( - (participant) => - participant.conversationId === conversationId && participant.deletedAt === null, + Layer.succeed( + ConnectParticipantRepo, + serviceShape({ + listByConversation: (conversationId: ConnectConversationId) => + Effect.succeed( + participants.filter( + (participant) => + participant.conversationId === conversationId && participant.deletedAt === null, + ), ), - ), - update: (patch: Partial & { id: ConnectParticipantId }) => - Effect.sync(() => { - const participant = participants.find((candidate) => candidate.id === patch.id) - if (!participant) throw new Error(`Missing participant ${patch.id}`) - Object.assign(participant, patch) - return participant - }), - findByChannelAndUser: () => Effect.succeed(Option.none()), - insert: () => Effect.die("not implemented"), - upsertByChannelAndUser: () => Effect.die("not implemented"), - })) + update: (patch: Partial & { id: ConnectParticipantId }) => + Effect.sync(() => { + const participant = participants.find((candidate) => candidate.id === patch.id) + if (!participant) throw new Error(`Missing participant ${patch.id}`) + Object.assign(participant, patch) + return participant + }), + findByChannelAndUser: () => Effect.succeed(Option.none()), + insert: () => Effect.die("not implemented"), + upsertByChannelAndUser: () => Effect.die("not implemented"), + }), + ) const makeChannelAccessSyncLayer = (syncedChannels: ChannelId[]) => - Layer.succeed(ChannelAccessSyncService, serviceShape({ - syncChannel: (channelId: ChannelId) => - Effect.sync(() => { - syncedChannels.push(channelId) - }), - })) + Layer.succeed( + ChannelAccessSyncService, + serviceShape({ + syncChannel: (channelId: ChannelId) => + Effect.sync(() => { + syncedChannels.push(channelId) + }), + }), + ) const makeServiceLayer = (params: { conversation: MutableConversation @@ -175,9 +201,8 @@ const makeServiceLayer = (params: { Layer.provide(makeOrgResolverLayer()), ) -const useService = ( - fn: (service: ConnectConversationServiceShape) => Effect.Effect, -) => ConnectConversationService.use(fn) +const useService = (fn: (service: ConnectConversationServiceShape) => Effect.Effect) => + ConnectConversationService.use(fn) describe("ConnectConversationService", () => { it("returns the existing mount when conversation creation races on unique constraints", async () => { @@ -210,70 +235,82 @@ describe("ConnectConversationService", () => { const layer = Layer.effect(ConnectConversationService, ConnectConversationService.make).pipe( Layer.provide( - Layer.succeed(ChannelRepo, serviceShape({ - findById: () => - Effect.succeed( - Option.some({ - id: HOST_CHANNEL_ID, - organizationId: HOST_ORG_ID, - }), - ), - })), + Layer.succeed( + ChannelRepo, + serviceShape({ + findById: () => + Effect.succeed( + Option.some({ + id: HOST_CHANNEL_ID, + organizationId: HOST_ORG_ID, + }), + ), + }), + ), ), Layer.provide( - Layer.succeed(ConnectConversationRepo, serviceShape({ - insert: () => - Effect.fail( - new Database.DatabaseError({ - type: "unique_violation", - cause: { constraint_name: "connect_conversations_host_channel_unique" }, - }), - ), - findByHostChannel: () => Effect.succeed(Option.some(existingConversation)), - })), + Layer.succeed( + ConnectConversationRepo, + serviceShape({ + insert: () => + Effect.fail( + new Database.DatabaseError({ + type: "unique_violation", + cause: { constraint_name: "connect_conversations_host_channel_unique" }, + }), + ), + findByHostChannel: () => Effect.succeed(Option.some(existingConversation)), + }), + ), ), Layer.provide( Layer.succeed( ConnectConversationChannelRepo, serviceShape({ - findByChannelId: () => - Effect.sync(() => { - findByChannelCalls += 1 - return findByChannelCalls === 1 ? Option.none() : Option.some(existingMount) - }), - insert: () => - Effect.fail( - new Database.DatabaseError({ - type: "unique_violation", - cause: { constraint_name: "connect_conv_channels_channel_unique" }, + findByChannelId: () => + Effect.sync(() => { + findByChannelCalls += 1 + return findByChannelCalls === 1 ? Option.none() : Option.some(existingMount) }), - ), - findByConversationId: () => Effect.succeed([existingMount]), + insert: () => + Effect.fail( + new Database.DatabaseError({ + type: "unique_violation", + cause: { constraint_name: "connect_conv_channels_channel_unique" }, + }), + ), + findByConversationId: () => Effect.succeed([existingMount]), }), ), ), Layer.provide(makeParticipantRepoLayer([])), Layer.provide( - Layer.succeed(MessageRepo, serviceShape({ - backfillConversationIdForChannel: ( - _channelId: ChannelId, - conversationId: ConnectConversationId, - ) => - Effect.sync(() => { - backfills.push({ kind: "message", conversationId }) - }), - })), + Layer.succeed( + MessageRepo, + serviceShape({ + backfillConversationIdForChannel: ( + _channelId: ChannelId, + conversationId: ConnectConversationId, + ) => + Effect.sync(() => { + backfills.push({ kind: "message", conversationId }) + }), + }), + ), ), Layer.provide( - Layer.succeed(MessageReactionRepo, serviceShape({ - backfillConversationIdForChannel: ( - _channelId: ChannelId, - conversationId: ConnectConversationId, - ) => - Effect.sync(() => { - backfills.push({ kind: "reaction", conversationId }) - }), - })), + Layer.succeed( + MessageReactionRepo, + serviceShape({ + backfillConversationIdForChannel: ( + _channelId: ChannelId, + conversationId: ConnectConversationId, + ) => + Effect.sync(() => { + backfills.push({ kind: "reaction", conversationId }) + }), + }), + ), ), Layer.provide(makeChannelAccessSyncLayer([])), Layer.provide(makeOrgResolverLayer()), @@ -299,83 +336,95 @@ describe("ConnectConversationService", () => { const layer = Layer.effect(ConnectConversationService, ConnectConversationService.make).pipe( Layer.provide( - Layer.succeed(ChannelRepo, serviceShape({ - findById: () => - Effect.succeed( - Option.some({ - id: HOST_CHANNEL_ID, - organizationId: HOST_ORG_ID, - }), - ), - })), + Layer.succeed( + ChannelRepo, + serviceShape({ + findById: () => + Effect.succeed( + Option.some({ + id: HOST_CHANNEL_ID, + organizationId: HOST_ORG_ID, + }), + ), + }), + ), ), Layer.provide( - Layer.succeed(ConnectConversationRepo, serviceShape({ - insert: () => - Effect.succeed([ - { - id: CONVERSATION_ID, - hostOrganizationId: HOST_ORG_ID, - hostChannelId: HOST_CHANNEL_ID, - status: "active", - settings: null, - createdBy: HOST_USER_ID, - createdAt: now, - updatedAt: now, - deletedAt: null, - }, - ]), - })), + Layer.succeed( + ConnectConversationRepo, + serviceShape({ + insert: () => + Effect.succeed([ + { + id: CONVERSATION_ID, + hostOrganizationId: HOST_ORG_ID, + hostChannelId: HOST_CHANNEL_ID, + status: "active", + settings: null, + createdBy: HOST_USER_ID, + createdAt: now, + updatedAt: now, + deletedAt: null, + }, + ]), + }), + ), ), Layer.provide( Layer.succeed( ConnectConversationChannelRepo, serviceShape({ - findByChannelId: () => Effect.succeed(Option.none()), - insert: () => - Effect.succeed([ - { - id: "00000000-0000-4000-8000-000000000412" as ConnectConversationChannelId, - conversationId: CONVERSATION_ID, - organizationId: HOST_ORG_ID, - channelId: HOST_CHANNEL_ID, - role: "host", - allowGuestMemberAdds: false, - isActive: true, - createdAt: now, - updatedAt: now, - deletedAt: null, - }, - ]), + findByChannelId: () => Effect.succeed(Option.none()), + insert: () => + Effect.succeed([ + { + id: "00000000-0000-4000-8000-000000000412" as ConnectConversationChannelId, + conversationId: CONVERSATION_ID, + organizationId: HOST_ORG_ID, + channelId: HOST_CHANNEL_ID, + role: "host", + allowGuestMemberAdds: false, + isActive: true, + createdAt: now, + updatedAt: now, + deletedAt: null, + }, + ]), }), ), ), Layer.provide(makeParticipantRepoLayer([])), Layer.provide( - Layer.succeed(MessageRepo, serviceShape({ - backfillConversationIdForChannel: () => - Effect.serviceOption(Database.TransactionContext).pipe( - Effect.tap((maybeTx) => - Effect.sync(() => { - transactionChecks.push(Option.isSome(maybeTx)) - }), + Layer.succeed( + MessageRepo, + serviceShape({ + backfillConversationIdForChannel: () => + Effect.serviceOption(Database.TransactionContext).pipe( + Effect.tap((maybeTx) => + Effect.sync(() => { + transactionChecks.push(Option.isSome(maybeTx)) + }), + ), + Effect.as(undefined), ), - Effect.as(undefined), - ), - })), + }), + ), ), Layer.provide( - Layer.succeed(MessageReactionRepo, serviceShape({ - backfillConversationIdForChannel: () => - Effect.serviceOption(Database.TransactionContext).pipe( - Effect.tap((maybeTx) => - Effect.sync(() => { - transactionChecks.push(Option.isSome(maybeTx)) - }), + Layer.succeed( + MessageReactionRepo, + serviceShape({ + backfillConversationIdForChannel: () => + Effect.serviceOption(Database.TransactionContext).pipe( + Effect.tap((maybeTx) => + Effect.sync(() => { + transactionChecks.push(Option.isSome(maybeTx)) + }), + ), + Effect.as(undefined), ), - Effect.as(undefined), - ), - })), + }), + ), ), Layer.provide(makeChannelAccessSyncLayer([])), Layer.provide(makeOrgResolverLayer()), @@ -433,20 +482,17 @@ describe("ConnectConversationService", () => { const layer = Layer.effect(ConnectConversationService, ConnectConversationService.make).pipe( Layer.provide(makeChannelRepoLayer()), Layer.provide( - Layer.succeed( - ConnectConversationRepo, - serviceShape({}), - ), + Layer.succeed(ConnectConversationRepo, serviceShape({})), ), Layer.provide( Layer.succeed( ConnectConversationChannelRepo, serviceShape({ - findByConversationId: () => - Effect.sync(() => { - mountFetchCount += 1 - return mounts - }), + findByConversationId: () => + Effect.sync(() => { + mountFetchCount += 1 + return mounts + }), }), ), ), @@ -454,11 +500,11 @@ describe("ConnectConversationService", () => { Layer.succeed( ConnectParticipantRepo, serviceShape({ - upsertByChannelAndUser: (row: ConnectParticipantUpsertInput) => - Effect.sync(() => { - upserts.push(row) - return row - }), + upsertByChannelAndUser: (row: ConnectParticipantUpsertInput) => + Effect.sync(() => { + upserts.push(row) + return row + }), }), ), ), diff --git a/apps/backend/src/services/message-outbox-dispatcher.test.ts b/apps/backend/src/services/message-outbox-dispatcher.test.ts index 6ae35a1d4..44cd0dee3 100644 --- a/apps/backend/src/services/message-outbox-dispatcher.test.ts +++ b/apps/backend/src/services/message-outbox-dispatcher.test.ts @@ -48,10 +48,13 @@ const runDispatcherEffect = ( Effect.provide(Layer.succeed(MessageSideEffectService, sideEffects)), Effect.provide(MessageOutboxRepo.layer), Effect.provide( - Layer.succeed(EnvVars, serviceShape({ - IS_DEV: true, - DATABASE_URL: Redacted.make(harness.container.getConnectionUri()), - })), + Layer.succeed( + EnvVars, + serviceShape({ + IS_DEV: true, + DATABASE_URL: Redacted.make(harness.container.getConnectionUri()), + }), + ), ), Effect.provide(harness.dbLayer), ), diff --git a/apps/backend/src/services/org-resolver.test.ts b/apps/backend/src/services/org-resolver.test.ts index d4b7efac3..2dbf5dc6e 100644 --- a/apps/backend/src/services/org-resolver.test.ts +++ b/apps/backend/src/services/org-resolver.test.ts @@ -15,12 +15,15 @@ const MESSAGE_ID = "00000000-0000-4000-8000-000000000601" as MessageId const CHANNEL_MEMBER_ID = "00000000-0000-4000-8000-000000000701" as ChannelMemberId const makeOrgMemberRepoLayer = (members: Record) => - Layer.succeed(OrganizationMemberRepo, serviceShape({ - findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { - const role = members[`${organizationId}:${userId}`] - return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) - }, - })) + Layer.succeed( + OrganizationMemberRepo, + serviceShape({ + findByOrgAndUser: (organizationId: OrganizationId, userId: UserId) => { + const role = members[`${organizationId}:${userId}`] + return Effect.succeed(role ? Option.some({ organizationId, userId, role }) : Option.none()) + }, + }), + ) const makeChannelRepoLayer = ( channels: Record< @@ -28,30 +31,41 @@ const makeChannelRepoLayer = ( { organizationId: OrganizationId; type: string; parentChannelId?: string | null; id: string } >, ) => - Layer.succeed(ChannelRepo, serviceShape({ - findById: (id: ChannelId) => { - const channel = channels[id] - return Effect.succeed(channel ? Option.some(channel) : Option.none()) - }, - })) + Layer.succeed( + ChannelRepo, + serviceShape({ + findById: (id: ChannelId) => { + const channel = channels[id] + return Effect.succeed(channel ? Option.some(channel) : Option.none()) + }, + }), + ) const makeChannelMemberRepoLayer = (memberships: Record) => - Layer.succeed(ChannelMemberRepo, serviceShape({ - findByChannelAndUser: (channelId: ChannelId, userId: UserId) => { - const key = `${channelId}:${userId}` - return Effect.succeed( - memberships[key] ? Option.some({ id: CHANNEL_MEMBER_ID, channelId, userId }) : Option.none(), - ) - }, - })) + Layer.succeed( + ChannelMemberRepo, + serviceShape({ + findByChannelAndUser: (channelId: ChannelId, userId: UserId) => { + const key = `${channelId}:${userId}` + return Effect.succeed( + memberships[key] + ? Option.some({ id: CHANNEL_MEMBER_ID, channelId, userId }) + : Option.none(), + ) + }, + }), + ) const makeMessageRepoLayer = (messages: Record) => - Layer.succeed(MessageRepo, serviceShape({ - findById: (id: MessageId) => { - const message = messages[id] - return Effect.succeed(message ? Option.some(message) : Option.none()) - }, - })) + Layer.succeed( + MessageRepo, + serviceShape({ + findById: (id: MessageId) => { + const message = messages[id] + return Effect.succeed(message ? Option.some(message) : Option.none()) + }, + }), + ) const makeResolverLayer = (opts: { members?: Record diff --git a/apps/backend/src/test/effect-helpers.ts b/apps/backend/src/test/effect-helpers.ts index 6df6c713f..b834d29b4 100644 --- a/apps/backend/src/test/effect-helpers.ts +++ b/apps/backend/src/test/effect-helpers.ts @@ -1,8 +1,7 @@ import { ConfigProvider, ServiceMap } from "effect" -export const serviceShape = ( - shape: unknown, -) => shape as ServiceMap.Service.Shape +export const serviceShape = (shape: unknown) => + shape as ServiceMap.Service.Shape export const configLayer = (values: Record) => ConfigProvider.layer(ConfigProvider.fromUnknown(values)) diff --git a/apps/backend/src/test/message-outbox-repo.test.ts b/apps/backend/src/test/message-outbox-repo.test.ts index 5c7fa5a35..8b67609a5 100644 --- a/apps/backend/src/test/message-outbox-repo.test.ts +++ b/apps/backend/src/test/message-outbox-repo.test.ts @@ -82,9 +82,9 @@ describe("MessageOutboxRepo", () => { ) expect(claimed).toHaveLength(3) - expect(claimed.map((event) => event.sequence)).toEqual( - [...claimed.map((event) => event.sequence)].sort((left, right) => left - right), - ) + expect(claimed.map((event) => event.sequence)).toEqual( + [...claimed.map((event) => event.sequence)].sort((left, right) => left - right), + ) expect(claimed.map((event) => event.eventType)).toEqual([ "message_created", "message_updated", diff --git a/apps/web/src/atoms/desktop-auth.ts b/apps/web/src/atoms/desktop-auth.ts index 1196f9d0f..398d3156c 100644 --- a/apps/web/src/atoms/desktop-auth.ts +++ b/apps/web/src/atoms/desktop-auth.ts @@ -24,26 +24,26 @@ import { forceRefreshEffect, getAccessToken as getAccessTokenPromise } from "~/l // ============================================================================ export interface DesktopTokens { - accessToken: string - refreshToken: string - expiresAt: number + accessToken: string + refreshToken: string + expiresAt: number } export type DesktopAuthStatus = "idle" | "loading" | "authenticated" | "error" export interface DesktopAuthError { - _tag: string - message: string + _tag: string + message: string } interface DesktopLoginOptions { - returnTo?: string - organizationId?: OrganizationId - invitationToken?: string + returnTo?: string + organizationId?: OrganizationId + invitationToken?: string } interface DesktopLogoutOptions { - redirectTo?: string + redirectTo?: string } // ============================================================================ @@ -84,11 +84,11 @@ export const isDesktopAuthenticatedAtom = Atom.make((get) => get(desktopTokensAt // ============================================================================ function toErrorInfo(error: unknown): { _tag: string; message: string } { - const e = error as { _tag?: string; message?: string } | undefined - return { - _tag: e?._tag ?? "UnknownError", - message: e?.message ?? "Unknown error", - } + const e = error as { _tag?: string; message?: string } | undefined + return { + _tag: e?._tag ?? "UnknownError", + message: e?.message ?? "Unknown error", + } } // ============================================================================ @@ -99,167 +99,163 @@ function toErrorInfo(error: unknown): { _tag: string; message: string } { * Action atom that initiates the desktop OAuth flow */ export const desktopLoginAtom = Atom.fn( - Effect.fnUntraced(function* (options: DesktopLoginOptions | undefined, get) { - if (!isTauri()) { - yield* Effect.log("[desktop-auth] Not in Tauri environment, skipping desktop login") - return - } - - get.set(desktopAuthStatusAtom, "loading") - get.set(desktopAuthErrorAtom, null) - - const loginEffect = Effect.gen(function* () { - const auth: TauriAuthService = yield* TauriAuth - const authResult = yield* auth.initiateAuth(options) - - const tokenStorage: TokenStorageService = yield* TokenStorage - const accessTokenOpt = yield* tokenStorage.getAccessToken - const refreshTokenOpt = yield* tokenStorage.getRefreshToken - const expiresAtOpt = yield* tokenStorage.getExpiresAt - - if ( - Option.isSome(accessTokenOpt) && - Option.isSome(refreshTokenOpt) && - Option.isSome(expiresAtOpt) - ) { - get.set(desktopTokensAtom, { - accessToken: accessTokenOpt.value, - refreshToken: refreshTokenOpt.value, - expiresAt: expiresAtOpt.value, - }) - get.set(desktopAuthStatusAtom, "authenticated") - } - - return authResult - }).pipe(Effect.provide(Layer.mergeAll(TauriAuthLive, TokenStorageLive))) - - const result = yield* loginEffect.pipe( - Effect.catch((error) => { - console.error("[desktop-auth] Login failed:", error) - const info = toErrorInfo(error) - get.set(desktopAuthStatusAtom, "error") - get.set(desktopAuthErrorAtom, info) - return Effect.fail(error) - }), - ) - - yield* Effect.log(`[desktop-auth] Login successful, navigating to: ${result.returnTo}`) - window.location.href = result.returnTo - }), + Effect.fnUntraced(function* (options: DesktopLoginOptions | undefined, get) { + if (!isTauri()) { + yield* Effect.log("[desktop-auth] Not in Tauri environment, skipping desktop login") + return + } + + get.set(desktopAuthStatusAtom, "loading") + get.set(desktopAuthErrorAtom, null) + + const loginEffect = Effect.gen(function* () { + const auth: TauriAuthService = yield* TauriAuth + const authResult = yield* auth.initiateAuth(options) + + const tokenStorage: TokenStorageService = yield* TokenStorage + const accessTokenOpt = yield* tokenStorage.getAccessToken + const refreshTokenOpt = yield* tokenStorage.getRefreshToken + const expiresAtOpt = yield* tokenStorage.getExpiresAt + + if ( + Option.isSome(accessTokenOpt) && + Option.isSome(refreshTokenOpt) && + Option.isSome(expiresAtOpt) + ) { + get.set(desktopTokensAtom, { + accessToken: accessTokenOpt.value, + refreshToken: refreshTokenOpt.value, + expiresAt: expiresAtOpt.value, + }) + get.set(desktopAuthStatusAtom, "authenticated") + } + + return authResult + }).pipe(Effect.provide(Layer.mergeAll(TauriAuthLive, TokenStorageLive))) + + const result = yield* loginEffect.pipe( + Effect.catch((error) => { + console.error("[desktop-auth] Login failed:", error) + const info = toErrorInfo(error) + get.set(desktopAuthStatusAtom, "error") + get.set(desktopAuthErrorAtom, info) + return Effect.fail(error) + }), + ) + + yield* Effect.log(`[desktop-auth] Login successful, navigating to: ${result.returnTo}`) + window.location.href = result.returnTo + }), ) /** * Action atom that performs desktop logout */ export const desktopLogoutAtom = Atom.fn( - Effect.fnUntraced(function* (options: DesktopLogoutOptions | undefined, get) { - if (!isTauri()) { - yield* Effect.log("[desktop-auth] Not in Tauri environment, skipping desktop logout") - return - } - - yield* Effect.gen(function* () { - const tokenStorage: TokenStorageService = yield* TokenStorage - yield* tokenStorage.clearTokens - }).pipe( - Effect.provide(TokenStorageLive), - Effect.catch(() => { - console.error("[desktop-auth] Failed to clear tokens") - return Effect.void - }), - ) - - get.set(desktopTokensAtom, null) - get.set(desktopAuthStatusAtom, "idle") - get.set(desktopAuthErrorAtom, null) - - const redirectTo = options?.redirectTo || "/" - yield* Effect.log(`[desktop-auth] Logout complete, redirecting to: ${redirectTo}`) - window.location.href = redirectTo - }), + Effect.fnUntraced(function* (options: DesktopLogoutOptions | undefined, get) { + if (!isTauri()) { + yield* Effect.log("[desktop-auth] Not in Tauri environment, skipping desktop logout") + return + } + + yield* Effect.gen(function* () { + const tokenStorage: TokenStorageService = yield* TokenStorage + yield* tokenStorage.clearTokens + }).pipe( + Effect.provide(TokenStorageLive), + Effect.catch(() => { + console.error("[desktop-auth] Failed to clear tokens") + return Effect.void + }), + ) + + get.set(desktopTokensAtom, null) + get.set(desktopAuthStatusAtom, "idle") + get.set(desktopAuthErrorAtom, null) + + const redirectTo = options?.redirectTo || "/" + yield* Effect.log(`[desktop-auth] Logout complete, redirecting to: ${redirectTo}`) + window.location.href = redirectTo + }), ) /** * Action atom that forces an immediate token refresh via AuthToken */ export const desktopForceRefreshAtom = Atom.fn( - Effect.fnUntraced(function* (_: void) { - if (!isTauri()) return false - return yield* forceRefreshEffect - }), + Effect.fnUntraced(function* (_: void) { + if (!isTauri()) return false + return yield* forceRefreshEffect + }), ) /** * Schema for clipboard auth payload */ const ClipboardAuthPayload = Schema.Struct({ - code: Schema.String, - state: Schema.Unknown, + code: Schema.String, + state: Schema.Unknown, }) interface ExchangeResult { - accessToken: string - refreshToken: string - expiresIn: number + accessToken: string + refreshToken: string + expiresIn: number } /** * Action atom that authenticates using clipboard data */ export const desktopLoginFromClipboardAtom = Atom.fn( - Effect.fnUntraced(function* (_: void, get) { - if (!isTauri()) return - - get.set(desktopAuthStatusAtom, "loading") - get.set(desktopAuthErrorAtom, null) - - const clipboardEffect = Effect.gen(function* () { - const clipboard = yield* Clipboard.Clipboard - const clipboardText = yield* clipboard.readString - - const rawJson = yield* Effect.try({ - try: () => JSON.parse(clipboardText), - catch: () => new Error("Invalid clipboard data - not valid JSON"), - }) - - const parsed = yield* Schema.decodeUnknownEffect(ClipboardAuthPayload)(rawJson).pipe( - Effect.mapError(() => new Error("Invalid clipboard data - missing code or state")), - ) - - const stateString = - typeof parsed.state === "string" ? parsed.state : JSON.stringify(parsed.state) - - const tokenExchange: TokenExchangeService = yield* TokenExchange - const tokens = yield* tokenExchange.exchangeCode(parsed.code, stateString) - - return tokens as ExchangeResult - }).pipe(Effect.provide(Layer.mergeAll(ClipboardLive, TokenExchangeLive, TokenStorageLive))) - - const result: ExchangeResult = yield* clipboardEffect.pipe( - Effect.catch((error) => { - console.error("[desktop-auth] Clipboard login failed:", error) - get.set(desktopAuthStatusAtom, "error") - get.set(desktopAuthErrorAtom, { - _tag: "ClipboardAuthError", - message: - error instanceof Error - ? error.message - : "Failed to authenticate from clipboard", - }) - return Effect.fail(error) - }), - ) - - get.set(desktopTokensAtom, { - accessToken: result.accessToken, - refreshToken: result.refreshToken, - expiresAt: Date.now() + result.expiresIn * 1000, - }) - get.set(desktopAuthStatusAtom, "authenticated") - - yield* Effect.log("[desktop-auth] Clipboard login successful") - window.location.href = "/" - }), + Effect.fnUntraced(function* (_: void, get) { + if (!isTauri()) return + + get.set(desktopAuthStatusAtom, "loading") + get.set(desktopAuthErrorAtom, null) + + const clipboardEffect = Effect.gen(function* () { + const clipboard = yield* Clipboard.Clipboard + const clipboardText = yield* clipboard.readString + + const rawJson = yield* Effect.try({ + try: () => JSON.parse(clipboardText), + catch: () => new Error("Invalid clipboard data - not valid JSON"), + }) + + const parsed = yield* Schema.decodeUnknownEffect(ClipboardAuthPayload)(rawJson).pipe( + Effect.mapError(() => new Error("Invalid clipboard data - missing code or state")), + ) + + const stateString = typeof parsed.state === "string" ? parsed.state : JSON.stringify(parsed.state) + + const tokenExchange: TokenExchangeService = yield* TokenExchange + const tokens = yield* tokenExchange.exchangeCode(parsed.code, stateString) + + return tokens as ExchangeResult + }).pipe(Effect.provide(Layer.mergeAll(ClipboardLive, TokenExchangeLive, TokenStorageLive))) + + const result: ExchangeResult = yield* clipboardEffect.pipe( + Effect.catch((error) => { + console.error("[desktop-auth] Clipboard login failed:", error) + get.set(desktopAuthStatusAtom, "error") + get.set(desktopAuthErrorAtom, { + _tag: "ClipboardAuthError", + message: error instanceof Error ? error.message : "Failed to authenticate from clipboard", + }) + return Effect.fail(error) + }), + ) + + get.set(desktopTokensAtom, { + accessToken: result.accessToken, + refreshToken: result.refreshToken, + expiresAt: Date.now() + result.expiresIn * 1000, + }) + get.set(desktopAuthStatusAtom, "authenticated") + + yield* Effect.log("[desktop-auth] Clipboard login successful") + window.location.href = "/" + }), ) // ============================================================================ @@ -267,44 +263,44 @@ export const desktopLoginFromClipboardAtom = Atom.fn( // ============================================================================ export const desktopInitAtom = Atom.make((get) => { - if (!isTauri()) return null - - const loadTokens = Effect.gen(function* () { - const tokenStorage: TokenStorageService = yield* TokenStorage - const accessTokenOpt = yield* tokenStorage.getAccessToken - const refreshTokenOpt = yield* tokenStorage.getRefreshToken - const expiresAtOpt = yield* tokenStorage.getExpiresAt - - if (Option.isSome(accessTokenOpt) && Option.isSome(refreshTokenOpt) && Option.isSome(expiresAtOpt)) { - get.set(desktopTokensAtom, { - accessToken: accessTokenOpt.value, - refreshToken: refreshTokenOpt.value, - expiresAt: expiresAtOpt.value, - }) - get.set(desktopAuthStatusAtom, "authenticated") - yield* Effect.log("[desktop-auth] Loaded tokens from storage") - } else { - get.set(desktopAuthStatusAtom, "idle") - yield* Effect.log("[desktop-auth] No stored tokens found") - } - }).pipe( - Effect.provide(TokenStorageLive), - Effect.catch((error) => { - console.error("[desktop-auth] Failed to load tokens:", error) - const info = toErrorInfo(error) - get.set(desktopAuthStatusAtom, "error") - get.set(desktopAuthErrorAtom, info) - return Effect.void - }), - ) - - const fiber = runtime.runFork(loadTokens) - - get.addFinalizer(() => { - fiber.interruptUnsafe() - }) - - return null + if (!isTauri()) return null + + const loadTokens = Effect.gen(function* () { + const tokenStorage: TokenStorageService = yield* TokenStorage + const accessTokenOpt = yield* tokenStorage.getAccessToken + const refreshTokenOpt = yield* tokenStorage.getRefreshToken + const expiresAtOpt = yield* tokenStorage.getExpiresAt + + if (Option.isSome(accessTokenOpt) && Option.isSome(refreshTokenOpt) && Option.isSome(expiresAtOpt)) { + get.set(desktopTokensAtom, { + accessToken: accessTokenOpt.value, + refreshToken: refreshTokenOpt.value, + expiresAt: expiresAtOpt.value, + }) + get.set(desktopAuthStatusAtom, "authenticated") + yield* Effect.log("[desktop-auth] Loaded tokens from storage") + } else { + get.set(desktopAuthStatusAtom, "idle") + yield* Effect.log("[desktop-auth] No stored tokens found") + } + }).pipe( + Effect.provide(TokenStorageLive), + Effect.catch((error) => { + console.error("[desktop-auth] Failed to load tokens:", error) + const info = toErrorInfo(error) + get.set(desktopAuthStatusAtom, "error") + get.set(desktopAuthErrorAtom, info) + return Effect.void + }), + ) + + const fiber = runtime.runFork(loadTokens) + + get.addFinalizer(() => { + fiber.interruptUnsafe() + }) + + return null }).pipe(Atom.keepAlive) // ============================================================================ @@ -312,39 +308,39 @@ export const desktopInitAtom = Atom.make((get) => { // ============================================================================ export const desktopTokenSchedulerAtom = Atom.make((get) => { - const tokens = get(desktopTokensAtom) + const tokens = get(desktopTokensAtom) - if (!tokens || !isTauri()) return null + if (!tokens || !isTauri()) return null - const timeUntilRefresh = tokens.expiresAt - Date.now() - REFRESH_BUFFER_MS + const timeUntilRefresh = tokens.expiresAt - Date.now() - REFRESH_BUFFER_MS - if (timeUntilRefresh <= 0) { - runtime.runFork( - Effect.gen(function* () { - yield* Effect.log("[desktop-auth] Token expired or expiring soon, refreshing now") - yield* forceRefreshEffect - }), - ) - return { scheduledFor: Date.now(), immediate: true } - } + if (timeUntilRefresh <= 0) { + runtime.runFork( + Effect.gen(function* () { + yield* Effect.log("[desktop-auth] Token expired or expiring soon, refreshing now") + yield* forceRefreshEffect + }), + ) + return { scheduledFor: Date.now(), immediate: true } + } - const minutes = Math.round(timeUntilRefresh / 1000 / 60) - const scheduledFor = tokens.expiresAt - REFRESH_BUFFER_MS + const minutes = Math.round(timeUntilRefresh / 1000 / 60) + const scheduledFor = tokens.expiresAt - REFRESH_BUFFER_MS - const refreshSchedule = Effect.gen(function* () { - yield* Effect.log(`[desktop-auth] Scheduling refresh in ${minutes} minutes`) - yield* Effect.sleep(Duration.millis(timeUntilRefresh)) - yield* Effect.log("[desktop-auth] Scheduled refresh triggered") - yield* forceRefreshEffect - }) + const refreshSchedule = Effect.gen(function* () { + yield* Effect.log(`[desktop-auth] Scheduling refresh in ${minutes} minutes`) + yield* Effect.sleep(Duration.millis(timeUntilRefresh)) + yield* Effect.log("[desktop-auth] Scheduled refresh triggered") + yield* forceRefreshEffect + }) - const fiber = runtime.runFork(refreshSchedule) + const fiber = runtime.runFork(refreshSchedule) - get.addFinalizer(() => { - fiber.interruptUnsafe() - }) + get.addFinalizer(() => { + fiber.interruptUnsafe() + }) - return { scheduledFor, immediate: false } + return { scheduledFor, immediate: false } }).pipe(Atom.keepAlive) // ============================================================================ @@ -352,30 +348,30 @@ export const desktopTokenSchedulerAtom = Atom.make((get) => { // ============================================================================ export const getDesktopAccessToken = (): Promise => { - if (!isTauri()) return Promise.resolve(null) - return getAccessTokenPromise() + if (!isTauri()) return Promise.resolve(null) + return getAccessTokenPromise() } export const clearDesktopTokens = (): Promise => { - if (!isTauri()) return Promise.resolve() - - return runtime.runPromise( - Effect.gen(function* () { - const tokenStorage: TokenStorageService = yield* TokenStorage - yield* tokenStorage.clearTokens - }).pipe( - Effect.provide(TokenStorageLive), - Effect.catch(() => { - console.error("[desktop-auth] Failed to clear tokens during recovery") - return Effect.void - }), - Effect.ensuring( - Effect.sync(() => { - appRegistry.set(desktopTokensAtom, null) - appRegistry.set(desktopAuthStatusAtom, "idle") - appRegistry.set(desktopAuthErrorAtom, null) - }), - ), - ), - ) as Promise + if (!isTauri()) return Promise.resolve() + + return runtime.runPromise( + Effect.gen(function* () { + const tokenStorage: TokenStorageService = yield* TokenStorage + yield* tokenStorage.clearTokens + }).pipe( + Effect.provide(TokenStorageLive), + Effect.catch(() => { + console.error("[desktop-auth] Failed to clear tokens during recovery") + return Effect.void + }), + Effect.ensuring( + Effect.sync(() => { + appRegistry.set(desktopTokensAtom, null) + appRegistry.set(desktopAuthStatusAtom, "idle") + appRegistry.set(desktopAuthErrorAtom, null) + }), + ), + ), + ) as Promise } diff --git a/apps/web/src/components/chat/pinned-messages-modal.tsx b/apps/web/src/components/chat/pinned-messages-modal.tsx index c9ad6a2a9..6c22f2d0c 100644 --- a/apps/web/src/components/chat/pinned-messages-modal.tsx +++ b/apps/web/src/components/chat/pinned-messages-modal.tsx @@ -147,7 +147,10 @@ export function PinnedMessagesModal() { : "Unknown"} - {format(toDate(pinnedMessage.message.createdAt), "HH:mm")} + {format( + toDate(pinnedMessage.message.createdAt), + "HH:mm", + )} {isEdited && " (edited)"}

@@ -161,7 +164,10 @@ export function PinnedMessagesModal() { {/* Pinned Date */}
Pinned{" "} - {format(toDate(pinnedMessage.pinned.pinnedAt), "MMM d 'at' h:mm a")} + {format( + toDate(pinnedMessage.pinned.pinnedAt), + "MMM d 'at' h:mm a", + )}
) diff --git a/apps/web/src/components/onboarding/org-setup-step.tsx b/apps/web/src/components/onboarding/org-setup-step.tsx index 984f4a077..819692474 100644 --- a/apps/web/src/components/onboarding/org-setup-step.tsx +++ b/apps/web/src/components/onboarding/org-setup-step.tsx @@ -74,12 +74,12 @@ export function OrgSetupStep({ isPublic: false, }, }) - exitToast(exit) - .onErrorTag("OrganizationSlugAlreadyExistsError", () => ({ - title: "Slug already taken", - description: "That workspace URL is already in use. Please choose a different one.", - isRetryable: false, - })) + exitToast(exit) + .onErrorTag("OrganizationSlugAlreadyExistsError", () => ({ + title: "Slug already taken", + description: "That workspace URL is already in use. Please choose a different one.", + isRetryable: false, + })) .run() if (Exit.isSuccess(exit)) { diff --git a/apps/web/src/components/theme-provider.tsx b/apps/web/src/components/theme-provider.tsx index c6342ac73..57c2972ff 100644 --- a/apps/web/src/components/theme-provider.tsx +++ b/apps/web/src/components/theme-provider.tsx @@ -16,9 +16,7 @@ type ThemeProviderProps = { const ThemeSchema = Schema.Literals(["dark", "light", "system"]) -const HexColorSchema = Schema.String.pipe( - Schema.check(Schema.isPattern(/^#[0-9A-Fa-f]{6}$/)), -) +const HexColorSchema = Schema.String.pipe(Schema.check(Schema.isPattern(/^#[0-9A-Fa-f]{6}$/))) const GrayPaletteSchema = Schema.Literals([ "gray", diff --git a/apps/web/src/lib/auth-token.ts b/apps/web/src/lib/auth-token.ts index 36f83d37f..b0e9b2500 100644 --- a/apps/web/src/lib/auth-token.ts +++ b/apps/web/src/lib/auth-token.ts @@ -197,12 +197,15 @@ const forceRefreshEffect: Effect.Effect = Effect.gen(function* () { Effect.gen(function* () { const tokenExchange: TokenExchangeService = yield* TokenExchange - const refreshResult: RefreshResult = yield* tokenExchange.refreshToken(refreshTokenOpt.value).pipe( - Effect.map((tokens): RefreshResult => ({ success: true, tokens })), - Effect.catch((error): Effect.Effect => - Effect.succeed({ success: false, error: error as ErrorLike }), - ), - ) + const refreshResult: RefreshResult = yield* tokenExchange + .refreshToken(refreshTokenOpt.value) + .pipe( + Effect.map((tokens): RefreshResult => ({ success: true, tokens })), + Effect.catch( + (error): Effect.Effect => + Effect.succeed({ success: false, error: error as ErrorLike }), + ), + ) if (refreshResult.success) { const { tokens } = refreshResult diff --git a/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/connect.tsx b/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/connect.tsx index c69011474..3d64766b2 100644 --- a/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/connect.tsx +++ b/apps/web/src/routes/_app/$orgSlug/channels/$channelId/settings/connect.tsx @@ -334,7 +334,7 @@ function InviteRow({ - {toDate(invite.createdAt).toLocaleDateString()} + {toDate(invite.createdAt).toLocaleDateString()} {invite.status === "pending" && ( diff --git a/apps/web/src/routes/_app/$orgSlug/settings/custom-emojis.tsx b/apps/web/src/routes/_app/$orgSlug/settings/custom-emojis.tsx index fd9218592..cfb2fb168 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/custom-emojis.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/custom-emojis.tsx @@ -209,7 +209,10 @@ function CustomEmojisSettings() { const deletedExistsError = createResult.cause.reasons .filter(Cause.isFailReason) .map((reason) => reason.error) - .find((error): error is CustomEmojiDeletedExistsError => error instanceof CustomEmojiDeletedExistsError) + .find( + (error): error is CustomEmojiDeletedExistsError => + error instanceof CustomEmojiDeletedExistsError, + ) if (deletedExistsError) { setRestoreTarget({ diff --git a/apps/web/src/utils/status.ts b/apps/web/src/utils/status.ts index 8d9b2c41e..c89836c7f 100644 --- a/apps/web/src/utils/status.ts +++ b/apps/web/src/utils/status.ts @@ -66,7 +66,9 @@ export function getStatusLabel(status?: PresenceStatus | string): string { /** * Formats the status expiration time in a human-readable way */ -export function formatStatusExpiration(expiresAt: Date | { epochMilliseconds: number } | null | undefined): string | null { +export function formatStatusExpiration( + expiresAt: Date | { epochMilliseconds: number } | null | undefined, +): string | null { if (!expiresAt) return null const now = new Date() diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.result.test.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.result.test.ts index 403b663c6..1dd05ab33 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.result.test.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.result.test.ts @@ -9,11 +9,7 @@ * @since 1.0.0 */ -import { - Atom, - AsyncResult as Result, - AtomRegistry as Registry, -} from "effect/unstable/reactivity" +import { Atom, AsyncResult as Result, AtomRegistry as Registry } from "effect/unstable/reactivity" import { type Collection, createCollection, eq, type NonSingleResult } from "@tanstack/db" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { makeCollectionAtom, makeQuery, makeQueryConditional, makeQueryUnsafe } from "./AtomTanStackDB" diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.subscription.test.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.subscription.test.ts index b6472af8a..9b34e5aa7 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.subscription.test.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.subscription.test.ts @@ -9,11 +9,7 @@ * @since 1.0.0 */ -import { - Atom, - AsyncResult as Result, - AtomRegistry as Registry, -} from "effect/unstable/reactivity" +import { Atom, AsyncResult as Result, AtomRegistry as Registry } from "effect/unstable/reactivity" import { type Collection, createCollection, eq, type NonSingleResult } from "@tanstack/db" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { makeCollectionAtom, makeQuery } from "./AtomTanStackDB" diff --git a/libs/tanstack-db-atom/src/AtomTanStackDB.timing.test.ts b/libs/tanstack-db-atom/src/AtomTanStackDB.timing.test.ts index 5198f2867..e929f408f 100644 --- a/libs/tanstack-db-atom/src/AtomTanStackDB.timing.test.ts +++ b/libs/tanstack-db-atom/src/AtomTanStackDB.timing.test.ts @@ -9,11 +9,7 @@ * @since 1.0.0 */ -import { - Atom, - AsyncResult as Result, - AtomRegistry as Registry, -} from "effect/unstable/reactivity" +import { Atom, AsyncResult as Result, AtomRegistry as Registry } from "effect/unstable/reactivity" import { type Collection, createCollection, eq, type NonSingleResult } from "@tanstack/db" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { makeCollectionAtom, makeQuery } from "./AtomTanStackDB" diff --git a/packages/actors/src/actors/message-actor.create-conn-state.test.ts b/packages/actors/src/actors/message-actor.create-conn-state.test.ts index 3ed3e20f0..c5d3b24ca 100644 --- a/packages/actors/src/actors/message-actor.create-conn-state.test.ts +++ b/packages/actors/src/actors/message-actor.create-conn-state.test.ts @@ -63,11 +63,7 @@ const loadCreateConnState = async () => { const validateBotToken = ( token: string, - ): EffectType.Effect< - BotClient, - BotTokenValidationError | ConfigError, - HttpClientType.HttpClient - > => + ): EffectType.Effect => Effect.gen(function* () { yield* HttpClient.HttpClient @@ -152,9 +148,7 @@ const loadCreateConnState = async () => { type: "user" as const, workosUserId: decodeWorkOsUserId(String(claims.sub)), workosOrganizationId: - typeof claims.org_id === "string" - ? decodeWorkOsOrganizationId(claims.org_id) - : null, + typeof claims.org_id === "string" ? decodeWorkOsOrganizationId(claims.org_id) : null, role: claims.role === "admin" ? "admin" : "member", } }) @@ -196,17 +190,13 @@ const loadCreateConnState = async () => { } }) const mod = await import("./message-actor.ts") - return (mod.messageActor as { - config: { - createConnState: ( - context: unknown, - params: { token?: string }, - ) => Promise + return ( + mod.messageActor as { + config: { + createConnState: (context: unknown, params: { token?: string }) => Promise + } } - }).config.createConnState as ( - context: unknown, - params: { token?: string }, - ) => Promise + ).config.createConnState as (context: unknown, params: { token?: string }) => Promise } afterEach(() => { From 9d7ffe36a29549caaf357fe6396b8ff8b7d7935b Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 17 Mar 2026 01:28:28 +0100 Subject: [PATCH 26/34] bette rlogin stuff --- apps/backend/src/index.ts | 2 + apps/backend/src/routes/auth.http.ts | 238 ++++++--- .../services/auth-redemption-store.test.ts | 256 ++++++++++ .../src/services/auth-redemption-store.ts | 456 ++++++++++++++++++ apps/web/src/atoms/web-callback-atoms.ts | 376 ++++++++------- apps/web/src/lib/auth-errors.ts | 63 +++ .../services/desktop/token-exchange.test.ts | 155 ++++++ .../lib/services/desktop/token-exchange.ts | 254 +++++----- .../lib/web-callback-single-flight.test.ts | 75 +++ .../web/src/lib/web-callback-single-flight.ts | 81 ++++ .../src/lib/web-login-single-flight.test.ts | 36 ++ apps/web/src/lib/web-login-single-flight.ts | 49 ++ apps/web/src/routes/auth/callback.test.tsx | 168 +++++++ apps/web/src/routes/auth/callback.tsx | 25 +- apps/web/src/routes/auth/login.test.tsx | 78 +++ apps/web/src/routes/auth/login.tsx | 30 +- packages/domain/src/errors.ts | 24 + packages/domain/src/http/auth.ts | 22 +- 18 files changed, 1999 insertions(+), 389 deletions(-) create mode 100644 apps/backend/src/services/auth-redemption-store.test.ts create mode 100644 apps/backend/src/services/auth-redemption-store.ts create mode 100644 apps/web/src/lib/auth-errors.ts create mode 100644 apps/web/src/lib/services/desktop/token-exchange.test.ts create mode 100644 apps/web/src/lib/web-callback-single-flight.test.ts create mode 100644 apps/web/src/lib/web-callback-single-flight.ts create mode 100644 apps/web/src/lib/web-login-single-flight.test.ts create mode 100644 apps/web/src/lib/web-login-single-flight.ts create mode 100644 apps/web/src/routes/auth/callback.test.tsx create mode 100644 apps/web/src/routes/auth/login.test.tsx diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 46c3d1293..f7116e032 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -80,6 +80,7 @@ import { RateLimiter } from "./services/rate-limiter" import { SessionManager } from "./services/session-manager" import { WebhookBotService } from "./services/webhook-bot-service" import { BotGatewayService } from "./services/bot-gateway-service" +import { AuthRedemptionStore } from "./services/auth-redemption-store" import { ChannelAccessSyncService } from "./services/channel-access-sync" import { ConnectConversationService } from "./services/connect-conversation-service" import { OrgResolver } from "./services/org-resolver" @@ -189,6 +190,7 @@ const MainLive = Layer.mergeAll( PolicyLive, MockDataGenerator.layer, WorkOSAuth.layer, + AuthRedemptionStore.layer, WorkOSClient.layer, WorkOSSync.layer, WorkOSWebhookVerifier.layer, diff --git a/apps/backend/src/routes/auth.http.ts b/apps/backend/src/routes/auth.http.ts index 5b6f5a0d9..04eb67431 100644 --- a/apps/backend/src/routes/auth.http.ts +++ b/apps/backend/src/routes/auth.http.ts @@ -1,14 +1,70 @@ +import { createHash } from "node:crypto" import { HttpApiBuilder } from "effect/unstable/httpapi" import { HttpServerResponse } from "effect/unstable/http" import { getJwtExpiry } from "@hazel/auth" import { UserRepo } from "@hazel/backend-core" +import { TokenResponse } from "@hazel/domain/http" import { WorkOSUserId } from "@hazel/schema" -import { InternalServerError, OAuthCodeExpiredError, UnauthorizedError } from "@hazel/domain" -import { Config, Effect, Option, Schema } from "effect" +import { + InternalServerError, + OAuthCodeExpiredError, + UnauthorizedError, +} from "@hazel/domain" +import { Config, Effect, Schema } from "effect" import { HazelApi } from "../api" -import { AuthState, DesktopAuthState, RelativeUrl } from "../lib/schema" +import { RelativeUrl } from "../lib/schema" +import { AuthRedemptionStore } from "../services/auth-redemption-store" import { WorkOSAuth as WorkOS } from "../services/workos-auth" +type TokenExchangeResponse = Schema.Schema.Type +type AuthHeaders = { + readonly "x-auth-attempt-id"?: string +} + +const hashValue = (value: string): string => createHash("sha256").update(value).digest("hex").slice(0, 12) +const getAttemptId = (headers: AuthHeaders | undefined): string => headers?.["x-auth-attempt-id"] ?? "missing" + +const getWorkOSCauseDetails = (cause: unknown) => { + const details = cause as { + status?: number + error?: string + errorDescription?: string + rawData?: { + error?: string + error_description?: string + } + message?: string + requestID?: string + } + + return { + status: details.status, + error: details.error ?? details.rawData?.error, + errorDescription: details.errorDescription ?? details.rawData?.error_description, + message: details.message ?? String(cause), + requestId: details.requestID ?? "", + } +} + +const mapWorkOSCodeExchangeError = ( + error: { + cause: unknown + }, +): OAuthCodeExpiredError | UnauthorizedError => { + const details = getWorkOSCauseDetails(error.cause) + + if (details.error === "invalid_grant") { + return new OAuthCodeExpiredError({ + message: details.errorDescription || "Authorization code expired or already used", + }) + } + + return new UnauthorizedError({ + message: "Failed to authenticate with WorkOS", + detail: details.errorDescription || details.message, + }) +} + export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => handlers .handle("login", ({ query }) => @@ -214,109 +270,115 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => ), ), ) - .handle("token", ({ payload }) => + .handle("token", ({ payload, headers }) => Effect.gen(function* () { const workos = yield* WorkOS + const authRedemptionStore = yield* AuthRedemptionStore const userRepo = yield* UserRepo const { code, state } = payload + const attemptId = getAttemptId(headers) const clientId = yield* Config.string("WORKOS_CLIENT_ID") - // Exchange code for tokens (without sealing - we want the JWT for desktop) - const authResponse = yield* workos - .call(async (client) => { - return await client.userManagement.authenticateWithCode({ - clientId, - code, - // Don't seal - we need the accessToken for desktop apps + yield* Effect.logInfo("[auth/token] Handling token exchange request", { + attemptId, + codeHash: hashValue(code), + stateHash: hashValue(state), + }) + + const tokens = yield* authRedemptionStore.exchangeCodeOnce( + { + code, + state, + attemptId, + }, + workos + .call(async (client) => { + return await client.userManagement.authenticateWithCode({ + clientId, + code, + // Don't seal - we need the accessToken for desktop apps + }) }) - }) - .pipe( - Effect.catchTag( - "WorkOSAuthError", - (error): Effect.Effect => { - const errorStr = String(error.cause) - // Detect expired/invalid code from WorkOS (invalid_grant) - if (errorStr.includes("invalid_grant")) { - return Effect.fail( - new OAuthCodeExpiredError({ - message: "Authorization code expired or already used", - }), - ) + .pipe( + Effect.catchTag("WorkOSAuthError", (error) => + Effect.fail(mapWorkOSCodeExchangeError(error)), + ), + Effect.map((authResponse): TokenExchangeResponse => { + const expiresIn = + getJwtExpiry(authResponse.accessToken) - Math.floor(Date.now() / 1000) + + return { + accessToken: authResponse.accessToken, + refreshToken: authResponse.refreshToken!, + expiresIn, + user: { + id: authResponse.user.id, + email: authResponse.user.email, + firstName: authResponse.user.firstName || "", + lastName: authResponse.user.lastName || "", + }, } - return Effect.fail( - new UnauthorizedError({ - message: "Failed to authenticate with WorkOS", - detail: errorStr, + }), + Effect.catchTag("UnauthorizedError", (error) => + Effect.fail( + new InternalServerError({ + message: error.message, + detail: error.detail, + cause: error, }), - ) - }, + ), + ), ), - ) + ) + + yield* Effect.logInfo("[auth/token] Ensuring local user exists", { + attemptId, + workosUserId: tokens.user.id, + }) - const { user: workosUser, accessToken, refreshToken } = authResponse + const workosUser = tokens.user const workosUserId = Schema.decodeUnknownSync(WorkOSUserId)(workosUser.id) - // Ensure user exists in our DB - const userOption = yield* userRepo.findByWorkOSUserId(workosUserId).pipe( + yield* userRepo.upsertWorkOSUser({ + externalId: workosUserId, + email: workosUser.email, + firstName: workosUser.firstName || "", + lastName: workosUser.lastName || "", + avatarUrl: null, + userType: "user", + settings: null, + isOnboarded: false, + timezone: null, + deletedAt: null, + }).pipe( Effect.catchTags({ DatabaseError: (err) => Effect.fail( new InternalServerError({ - message: "Failed to query user", + message: "Failed to upsert user after OAuth redemption", detail: String(err), }), ), }), ) - yield* Option.match(userOption, { - onNone: () => - userRepo - .upsertWorkOSUser({ - externalId: workosUserId, - email: workosUser.email, - firstName: workosUser.firstName || "", - lastName: workosUser.lastName || "", - avatarUrl: workosUser.profilePictureUrl?.trim() - ? workosUser.profilePictureUrl - : null, - userType: "user", - settings: null, - isOnboarded: false, - timezone: null, - deletedAt: null, - }) - .pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new InternalServerError({ - message: "Failed to create user", - detail: String(err), - }), - ), - }), - ), - onSome: (user) => Effect.succeed(user), + yield* Effect.logInfo("[auth/token] Token exchange request completed", { + attemptId, + workosUserId: tokens.user.id, + outcome: "success", }) - // Calculate expires in seconds from JWT expiry - const expiresIn = getJwtExpiry(accessToken) - Math.floor(Date.now() / 1000) - - return { - accessToken, - refreshToken: refreshToken!, - expiresIn, - user: { - id: workosUser.id, - email: workosUser.email, - firstName: workosUser.firstName || "", - lastName: workosUser.lastName || "", - }, - } + return tokens }).pipe( + Effect.tapError((error) => + Effect.logError("[auth/token] Token exchange request failed", { + attemptId: getAttemptId(headers), + errorTag: error._tag, + message: error.message, + }), + ), Effect.catchTag("ConfigError", (err) => Effect.fail( new InternalServerError({ message: "Missing configuration", detail: String(err) }), @@ -324,13 +386,19 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => ), ), ) - .handle("refresh", ({ payload }) => + .handle("refresh", ({ payload, headers }) => Effect.gen(function* () { const workos = yield* WorkOS const { refreshToken } = payload + const attemptId = getAttemptId(headers) const clientId = yield* Config.string("WORKOS_CLIENT_ID") + yield* Effect.logInfo("[auth/refresh] Handling refresh request", { + attemptId, + refreshTokenHash: hashValue(refreshToken), + }) + // Exchange refresh token for new tokens const authResponse = yield* workos .call(async (client) => { @@ -352,12 +420,24 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => const expiresIn = getJwtExpiry(authResponse.accessToken) - Math.floor(Date.now() / 1000) + yield* Effect.logInfo("[auth/refresh] Refresh request completed", { + attemptId, + outcome: "success", + }) + return { accessToken: authResponse.accessToken, refreshToken: authResponse.refreshToken!, expiresIn, } }).pipe( + Effect.tapError((error) => + Effect.logError("[auth/refresh] Refresh request failed", { + attemptId: getAttemptId(headers), + errorTag: error._tag, + message: error.message, + }), + ), Effect.catchTag("ConfigError", (err) => Effect.fail( new InternalServerError({ message: "Missing configuration", detail: String(err) }), diff --git a/apps/backend/src/services/auth-redemption-store.test.ts b/apps/backend/src/services/auth-redemption-store.test.ts new file mode 100644 index 000000000..af035280d --- /dev/null +++ b/apps/backend/src/services/auth-redemption-store.test.ts @@ -0,0 +1,256 @@ +import { describe, expect, it, vi } from "vitest" +import { Effect, Layer, Schema } from "effect" +import { InternalServerError, OAuthCodeExpiredError, OAuthStateMismatchError } from "@hazel/domain" +import { TokenResponse } from "@hazel/domain/http" +import { serviceShape } from "../test/effect-helpers" + +vi.mock("@hazel/effect-bun", async () => { + const { Layer, ServiceMap } = await import("effect") + class Redis extends ServiceMap.Service< + Redis, + { + readonly get: (key: string) => unknown + readonly del: (key: string) => unknown + readonly send: (command: string, args: string[]) => T + } + >()("@hazel/effect-bun/Redis") {} + + return { + Redis: Object.assign(Redis, { + Default: Layer.empty, + }), + } +}) + +import { Redis } from "@hazel/effect-bun" +import { AuthRedemptionStore } from "./auth-redemption-store" + +type TokenExchangeResponse = Schema.Schema.Type + +interface RedisValue { + value: string + expiresAt: number | null +} + +const makeResponse = (): TokenExchangeResponse => ({ + accessToken: "access-token", + refreshToken: "refresh-token", + expiresIn: 3600, + user: { + id: "user_123", + email: "test@example.com", + firstName: "Test", + lastName: "User", + }, +}) + +const makeRedisLayer = () => { + const store = new Map() + + const getValue = (key: string): string | null => { + const entry = store.get(key) + if (!entry) return null + if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) { + store.delete(key) + return null + } + return entry.value + } + + const setValue = (key: string, value: string, ttlMs?: number) => { + store.set(key, { + value, + expiresAt: ttlMs === undefined ? null : Date.now() + ttlMs, + }) + } + + return Layer.succeed( + Redis, + serviceShape({ + get: (key: string) => Effect.succeed(getValue(key)), + del: (key: string) => + Effect.sync(() => { + store.delete(key) + }), + send: (command: string, args: string[]) => + Effect.sync(() => { + if (command === "EVAL") { + const [, , key, processingValue, ttlMs] = args + const existing = getValue(key) + if (existing === null) { + setValue(key, processingValue, Number(ttlMs)) + return ["claimed", ""] as T + } + return ["existing", existing] as T + } + + if (command === "SET") { + const [key, value, , ttlMs] = args + setValue(key, value, Number(ttlMs)) + return "OK" as T + } + + throw new Error(`Unsupported Redis command in test: ${command}`) + }), + }), + ) +} + +const makeStore = async () => + await Effect.runPromise( + Effect.gen(function* () { + return yield* AuthRedemptionStore + }).pipe( + Effect.provide(Layer.effect(AuthRedemptionStore, AuthRedemptionStore.make).pipe(Layer.provide(makeRedisLayer()))), + ), + ) + +describe("AuthRedemptionStore", () => { + it("deduplicates concurrent redemptions and calls WorkOS once", async () => { + const store = await makeStore() + const gate = Promise.withResolvers() + let calls = 0 + const response = makeResponse() + const exchange = Effect.gen(function* () { + calls += 1 + yield* Effect.promise(() => gate.promise) + return response + }) + + const first = Effect.runPromise( + store.exchangeCodeOnce({ code: "code-1", state: JSON.stringify({ returnTo: "/" }) }, exchange), + ) + const second = Effect.runPromise( + store.exchangeCodeOnce({ code: "code-1", state: JSON.stringify({ returnTo: "/" }) }, exchange), + ) + + gate.resolve() + const result = await Promise.all([first, second]) + + expect(result).toEqual([response, response]) + expect(calls).toBe(1) + }) + + it("returns cached success on a duplicate request after completion", async () => { + const store = await makeStore() + let calls = 0 + const response = makeResponse() + const exchange = Effect.gen(function* () { + calls += 1 + return response + }) + + const first = await Effect.runPromise( + store.exchangeCodeOnce({ code: "code-2", state: JSON.stringify({ returnTo: "/" }) }, exchange), + ) + const second = await Effect.runPromise( + store.exchangeCodeOnce({ code: "code-2", state: JSON.stringify({ returnTo: "/" }) }, exchange), + ) + + expect([first, second]).toEqual([response, response]) + expect(calls).toBe(1) + }) + + it("caches invalid_grant failures and replays them to duplicates", async () => { + const store = await makeStore() + let calls = 0 + const exchange = Effect.gen(function* () { + calls += 1 + return yield* Effect.fail( + new OAuthCodeExpiredError({ + message: "Authorization code expired or already used", + }), + ) + }) + + const first = await Effect.runPromise( + Effect.flip( + store.exchangeCodeOnce({ code: "code-3", state: JSON.stringify({ returnTo: "/" }) }, exchange), + ), + ) + const second = await Effect.runPromise( + Effect.flip( + store.exchangeCodeOnce({ code: "code-3", state: JSON.stringify({ returnTo: "/" }) }, exchange), + ), + ) + + expect(first).toEqual( + new OAuthCodeExpiredError({ + message: "Authorization code expired or already used", + }), + ) + expect(second).toEqual( + new OAuthCodeExpiredError({ + message: "Authorization code expired or already used", + }), + ) + expect(calls).toBe(1) + }) + + it("clears the processing lock on transient failures so retries can re-run", async () => { + const store = await makeStore() + let calls = 0 + const response = makeResponse() + + const first = await Effect.runPromise( + Effect.flip( + store.exchangeCodeOnce( + { code: "code-4", state: JSON.stringify({ returnTo: "/" }) }, + Effect.gen(function* () { + calls += 1 + return yield* Effect.fail( + new InternalServerError({ + message: "Temporary database failure", + }), + ) + }), + ), + ), + ) + + const second = await Effect.runPromise( + store.exchangeCodeOnce( + { code: "code-4", state: JSON.stringify({ returnTo: "/" }) }, + Effect.gen(function* () { + calls += 1 + return response + }), + ), + ) + + expect(first).toEqual( + new InternalServerError({ + message: "Temporary database failure", + }), + ) + expect(second).toEqual(response) + expect(calls).toBe(2) + }) + + it("rejects reused codes when the duplicate request has a different state payload", async () => { + const store = await makeStore() + + await Effect.runPromise( + store.exchangeCodeOnce( + { code: "code-5", state: JSON.stringify({ returnTo: "/" }) }, + Effect.succeed(makeResponse()), + ), + ) + + const result = await Effect.runPromise( + Effect.flip( + store.exchangeCodeOnce( + { code: "code-5", state: JSON.stringify({ returnTo: "/other" }) }, + Effect.succeed(makeResponse()), + ), + ), + ) + + expect(result).toEqual( + new OAuthStateMismatchError({ + message: + "Received a duplicate OAuth redemption with mismatched state. Please restart login.", + }), + ) + }) +}) diff --git a/apps/backend/src/services/auth-redemption-store.ts b/apps/backend/src/services/auth-redemption-store.ts new file mode 100644 index 000000000..c8d65cd01 --- /dev/null +++ b/apps/backend/src/services/auth-redemption-store.ts @@ -0,0 +1,456 @@ +import { createHash } from "node:crypto" +import { Redis, type RedisErrors } from "@hazel/effect-bun" +import { + InternalServerError, + OAuthCodeExpiredError, + OAuthRedemptionPendingError, + OAuthStateMismatchError, +} from "@hazel/domain" +import { TokenResponse } from "@hazel/domain/http" +import { Duration, Effect, Layer, Schema, ServiceMap } from "effect" + +const AUTH_REDEMPTION_PREFIX = "auth:redemption" +const PROCESSING_TTL_MS = 30_000 +const RESULT_TTL_MS = 5 * 60_000 +const POLL_INTERVAL = Duration.millis(50) +const POLL_TIMEOUT_MS = 2_000 + +const CLAIM_PROCESSING_SCRIPT = ` +local key = KEYS[1] +local processingValue = ARGV[1] +local ttlMs = ARGV[2] + +local existing = redis.call("GET", key) +if not existing then + redis.call("SET", key, processingValue, "PX", ttlMs) + return { "claimed", "" } +end + +return { "existing", existing } +` + +const PermanentFailureSchema = Schema.Struct({ + _tag: Schema.Literal("OAuthCodeExpiredError"), + message: Schema.String, +}) + +const ProcessingRecordSchema = Schema.Struct({ + status: Schema.Literal("processing"), + requestHash: Schema.String, + codeHash: Schema.String, + createdAt: Schema.Number, +}) + +const SucceededRecordSchema = Schema.Struct({ + status: Schema.Literal("succeeded"), + requestHash: Schema.String, + codeHash: Schema.String, + createdAt: Schema.Number, + response: TokenResponse, +}) + +const FailedPermanentRecordSchema = Schema.Struct({ + status: Schema.Literal("failed_permanent"), + requestHash: Schema.String, + codeHash: Schema.String, + createdAt: Schema.Number, + error: PermanentFailureSchema, +}) + +const StoredRedemptionSchema = Schema.Union([ + ProcessingRecordSchema, + SucceededRecordSchema, + FailedPermanentRecordSchema, +]) + +type StoredRedemption = Schema.Schema.Type +type TokenExchangeResponse = Schema.Schema.Type +type ClaimResult = { _tag: "claimed" } | { _tag: "existing"; record: StoredRedemption } +type ExchangeResult = + | { _tag: "success"; response: TokenExchangeResponse } + | { _tag: "expired"; error: OAuthCodeExpiredError } + | { _tag: "internal"; error: InternalServerError } + +const mapRedisError = (message: string) => (error: RedisErrors) => + new InternalServerError({ + message, + detail: String(error), + cause: error, + }) + +const hashString = (value: string): string => createHash("sha256").update(value).digest("hex") + +const normalizeJsonValue = (value: unknown): unknown => { + if (Array.isArray(value)) { + return value.map(normalizeJsonValue) + } + + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, nested]) => [key, normalizeJsonValue(nested)]), + ) + } + + return value +} + +const canonicalizeState = (state: string): string => { + try { + return JSON.stringify(normalizeJsonValue(JSON.parse(state))) + } catch { + return state + } +} + +const getCodeHash = (code: string): string => hashString(code) +const getStateHash = (state: string): string => hashString(canonicalizeState(state)) +const getRequestHash = (code: string, state: string): string => + hashString(JSON.stringify({ code, state: canonicalizeState(state) })) + +const getRedisKey = (codeHash: string): string => `${AUTH_REDEMPTION_PREFIX}:${codeHash}` +const shortHash = (value: string): string => value.slice(0, 12) + +const decodeStoredRedemption = (raw: string): Effect.Effect => + Effect.try({ + try: () => Schema.decodeSync(StoredRedemptionSchema)(JSON.parse(raw)), + catch: (cause) => + new InternalServerError({ + message: "Failed to decode cached auth redemption", + detail: String(cause), + cause, + }), + }) + +const encodeStoredRedemption = (record: StoredRedemption): Effect.Effect => + Effect.try({ + try: () => JSON.stringify(record), + catch: (cause) => + new InternalServerError({ + message: "Failed to encode auth redemption state", + detail: String(cause), + cause, + }), + }) + +const toPermanentFailure = ( + error: OAuthCodeExpiredError, +): Schema.Schema.Type => ({ + _tag: "OAuthCodeExpiredError", + message: error.message, +}) + +const ensureMatchingRequest = ( + record: StoredRedemption, + requestHash: string, +): Effect.Effect => + record.requestHash === requestHash + ? Effect.void + : Effect.fail( + new OAuthStateMismatchError({ + message: + "Received a duplicate OAuth redemption with mismatched state. Please restart login.", + }), + ) + +const revivePermanentFailure = ( + error: Schema.Schema.Type, +): OAuthCodeExpiredError => + new OAuthCodeExpiredError({ + message: error.message, + }) + +export class AuthRedemptionStore extends ServiceMap.Service()("AuthRedemptionStore", { + make: Effect.gen(function* () { + const redis = yield* Redis + + const writeRecord = (key: string, record: StoredRedemption, ttlMs: number) => + Effect.gen(function* () { + const value = yield* encodeStoredRedemption(record) + yield* redis + .send("SET", [key, value, "PX", String(ttlMs)]) + .pipe(Effect.mapError(mapRedisError("Failed to persist OAuth redemption state"))) + }) + + const deleteRecord = (key: string) => + redis.del(key).pipe(Effect.mapError(mapRedisError("Failed to clear OAuth redemption state"))) + + const readRecord = (key: string) => + Effect.gen(function* () { + const raw = yield* redis + .get(key) + .pipe(Effect.mapError(mapRedisError("Failed to read OAuth redemption state"))) + + if (raw === null) { + return null + } + + return yield* decodeStoredRedemption(raw).pipe( + Effect.catchTag("InternalServerError", (error) => + deleteRecord(key).pipe(Effect.flatMap(() => Effect.fail(error))), + ), + ) + }) + + const claimProcessing = (key: string, record: Schema.Schema.Type) => + Effect.gen(function* () { + const processingValue = yield* encodeStoredRedemption(record) + const [status, existingValue] = yield* redis + .send<[string, string]>("EVAL", [ + CLAIM_PROCESSING_SCRIPT, + "1", + key, + processingValue, + String(PROCESSING_TTL_MS), + ]) + .pipe(Effect.mapError(mapRedisError("Failed to claim OAuth redemption state"))) + + if (status === "claimed") { + return { _tag: "claimed" } satisfies ClaimResult + } + + const existingRecord = yield* decodeStoredRedemption(existingValue) + return { + _tag: "existing", + record: existingRecord, + } satisfies ClaimResult + }) + + const awaitCompletion = ( + key: string, + requestHash: string, + codeHash: string, + stateHash: string, + attemptId: string, + startedAt: number, + ): Effect.Effect< + TokenExchangeResponse | null, + OAuthCodeExpiredError | OAuthStateMismatchError | OAuthRedemptionPendingError | InternalServerError + > => + Effect.gen(function* () { + const current = yield* readRecord(key) + if (current === null) { + yield* Effect.logInfo("[auth/token] Redemption lock cleared before completion", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "lock_cleared", + }) + return null + } + + yield* ensureMatchingRequest(current, requestHash) + + switch (current.status) { + case "succeeded": + yield* Effect.logInfo("[auth/token] Reused cached OAuth redemption", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "awaited_success", + }) + return current.response + case "failed_permanent": + yield* Effect.logInfo("[auth/token] Reused cached OAuth redemption failure", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "awaited_failure", + errorTag: current.error._tag, + }) + return yield* Effect.fail(revivePermanentFailure(current.error)) + case "processing": + if (Date.now() - startedAt >= POLL_TIMEOUT_MS) { + yield* Effect.logError("[auth/token] OAuth redemption still pending after poll timeout", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "pending_timeout", + }) + return yield* Effect.fail( + new OAuthRedemptionPendingError({ + message: + "Another login callback is still finishing. Please retry in a moment.", + }), + ) + } + + yield* Effect.sleep(POLL_INTERVAL) + return yield* awaitCompletion(key, requestHash, codeHash, stateHash, attemptId, startedAt) + } + }) + + const exchangeCodeOnce = ( + params: { + code: string + state: string + attemptId?: string + }, + exchange: Effect.Effect, + ): Effect.Effect< + TokenExchangeResponse, + OAuthCodeExpiredError | OAuthStateMismatchError | OAuthRedemptionPendingError | InternalServerError, + R + > => + Effect.gen(function* () { + const codeHash = getCodeHash(params.code) + const stateHash = getStateHash(params.state) + const requestHash = getRequestHash(params.code, params.state) + const key = getRedisKey(codeHash) + const createdAt = Date.now() + const attemptId = params.attemptId ?? "missing" + + yield* Effect.logInfo("[auth/token] OAuth redemption requested", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + requestHash: shortHash(requestHash), + outcome: "requested", + }) + + const processingRecord = { + status: "processing" as const, + requestHash, + codeHash, + createdAt, + } + + const claimResult: ClaimResult = yield* claimProcessing(key, processingRecord) + if (claimResult._tag === "existing") { + yield* ensureMatchingRequest(claimResult.record, requestHash).pipe( + Effect.catchTag("OAuthStateMismatchError", (error) => + Effect.logError("[auth/token] OAuth redemption state mismatch", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "mismatch", + }).pipe(Effect.flatMap(() => Effect.fail(error))), + ), + ) + + if (claimResult.record.status === "processing") { + yield* Effect.logInfo("[auth/token] Waiting for in-flight OAuth redemption", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "waiting", + }) + const awaited = yield* awaitCompletion( + key, + requestHash, + codeHash, + stateHash, + attemptId, + Date.now(), + ) + if (awaited !== null) { + return awaited + } + return yield* exchangeCodeOnce(params, exchange) + } + + if (claimResult.record.status === "succeeded") { + yield* Effect.logInfo("[auth/token] Returning cached OAuth redemption", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "cached_success", + }) + return claimResult.record.response + } + + yield* Effect.logInfo("[auth/token] Returning cached OAuth redemption failure", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "cached_failure", + errorTag: claimResult.record.error._tag, + }) + return yield* Effect.fail(revivePermanentFailure(claimResult.record.error)) + } + + yield* Effect.logInfo("[auth/token] Claim acquired, redeeming with WorkOS", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "fresh", + }) + + const exchangeResult: ExchangeResult = yield* exchange.pipe( + Effect.map((response) => ({ _tag: "success", response }) satisfies ExchangeResult), + Effect.catchTag("OAuthCodeExpiredError", (error) => + Effect.succeed({ _tag: "expired", error } satisfies ExchangeResult), + ), + Effect.catchTag("InternalServerError", (error) => + Effect.succeed({ _tag: "internal", error } satisfies ExchangeResult), + ), + ) + + switch (exchangeResult._tag) { + case "success": + yield* writeRecord( + key, + { + status: "succeeded", + requestHash, + codeHash, + createdAt, + response: exchangeResult.response, + }, + RESULT_TTL_MS, + ) + yield* Effect.logInfo("[auth/token] OAuth redemption completed", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "succeeded", + }) + return exchangeResult.response + case "expired": + yield* writeRecord( + key, + { + status: "failed_permanent", + requestHash, + codeHash, + createdAt, + error: toPermanentFailure(exchangeResult.error), + }, + RESULT_TTL_MS, + ) + yield* Effect.logInfo("[auth/token] OAuth redemption completed with permanent failure", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "expired", + errorTag: exchangeResult.error._tag, + }) + return yield* Effect.fail(exchangeResult.error) + case "internal": + yield* deleteRecord(key) + yield* Effect.logError("[auth/token] OAuth redemption reset after transient failure", { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "transient_reset", + errorTag: exchangeResult.error._tag, + }) + return yield* Effect.fail(exchangeResult.error) + } + }).pipe( + Effect.annotateLogs({ + authAttemptId: params.attemptId ?? "missing", + authCodeHash: shortHash(getCodeHash(params.code)), + authStateHash: shortHash(getStateHash(params.state)), + }), + Effect.withSpan("AuthRedemptionStore.exchangeCodeOnce"), + ) + + return { + exchangeCodeOnce, + } + }), +}) { + static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(Redis.Default)) +} diff --git a/apps/web/src/atoms/web-callback-atoms.ts b/apps/web/src/atoms/web-callback-atoms.ts index b1f2f8ce5..21e797cfd 100644 --- a/apps/web/src/atoms/web-callback-atoms.ts +++ b/apps/web/src/atoms/web-callback-atoms.ts @@ -9,11 +9,21 @@ import { MissingAuthCodeError, OAuthCallbackError, OAuthCodeExpiredError, + OAuthRedemptionPendingError, + OAuthStateMismatchError, + TokenDecodeError, TokenExchangeError, } from "@hazel/domain/errors" import { Effect, Layer, type ServiceMap } from "effect" import { appRegistry } from "~/lib/registry" -import { runtime } from "~/lib/services/common/runtime" +import { getWebAuthErrorInfo, type WebAuthError } from "~/lib/auth-errors" +import { + getWebCallbackAttemptKey as buildWebCallbackAttemptKey, + resetAllWebCallbackAttempts, + resetWebCallbackAttempt, + runWebCallbackAttemptOnce, + type WebCallbackKeyParams, +} from "~/lib/web-callback-single-flight" import { TokenExchange } from "~/lib/services/desktop/token-exchange" import { WebTokenStorage } from "~/lib/services/web/token-storage" import { webAuthStatusAtom, webTokensAtom } from "./web-auth" @@ -69,66 +79,59 @@ type TokenExchangeService = ServiceMap.Service.Shape type WebTokenStorageService = ServiceMap.Service.Shape // ============================================================================ -// Error Handling +// Attempt Context // ============================================================================ -type CallbackError = OAuthCallbackError | MissingAuthCodeError | TokenExchangeError | OAuthCodeExpiredError +const callbackAttemptIds = new Map() -/** - * Get user-friendly error info from typed error - */ -function getErrorInfo(error: CallbackError): { - message: string - isRetryable: boolean -} { - switch (error._tag) { - case "OAuthCallbackError": - return { - message: error.errorDescription || error.error, - isRetryable: true, - } - case "MissingAuthCodeError": - return { - message: "No authorization code received. Please try again.", - isRetryable: true, - } - case "OAuthCodeExpiredError": - // Code expired or already used - user must restart the login flow - return { - message: "Login session expired. Please try logging in again.", - isRetryable: false, - } - case "TokenExchangeError": - return { - message: error.message || "Failed to exchange authorization code.", - isRetryable: true, - } +const getOrCreateCallbackAttemptId = (attemptKey: string): string => { + const existing = callbackAttemptIds.get(attemptKey) + if (existing) { + return existing } + + const attemptId = `web_callback_${crypto.randomUUID()}` + callbackAttemptIds.set(attemptKey, attemptId) + return attemptId } -// ============================================================================ -// Core Callback Logic -// ============================================================================ +const logWebCallback = ( + level: "Info" | "Error", + message: string, + fields: Record, +): void => { + const effect = + level === "Error" ? Effect.logError(message, fields) : Effect.logInfo(message, fields) + void Effect.runFork(effect) +} -/** - * Module-level Set to track codes that have been processed - * Prevents double-execution from React StrictMode or hot reload - */ -const processedCodes = new Set() +type WebCallbackResult = + | { success: true; returnTo: string } + | { success: false; error: WebAuthError } /** * Effect that handles the web callback - exchanges code for tokens and stores them */ -const exchangeAndStoreTokens = (code: string, stateString: string, returnTo: string) => +const exchangeAndStoreTokens = ( + code: string, + stateString: string, + returnTo: string, + attemptId: string, +) => Effect.gen(function* () { const tokenExchange: TokenExchangeService = yield* TokenExchange const tokenStorage: WebTokenStorageService = yield* WebTokenStorage - yield* Effect.log("[web-callback] Exchanging code for tokens...") + yield* Effect.logInfo("[web-callback] Exchanging code for tokens", { + attemptId, + returnTo, + }) - const tokens = yield* tokenExchange.exchangeCode(code, stateString) + const tokens = yield* tokenExchange.exchangeCode(code, stateString, attemptId) - yield* Effect.log("[web-callback] Storing tokens...") + yield* Effect.logInfo("[web-callback] Storing tokens", { + attemptId, + }) yield* tokenStorage.storeTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn) const expiresAt = Date.now() + tokens.expiresIn * 1000 @@ -139,103 +142,133 @@ const exchangeAndStoreTokens = (code: string, stateString: string, returnTo: str }) appRegistry.set(webAuthStatusAtom, "authenticated") - yield* Effect.log("[web-callback] Token exchange successful") + yield* Effect.logInfo("[web-callback] Token exchange successful", { + attemptId, + returnTo, + }) return { success: true as const, returnTo } }) -const handleCallback = (params: WebCallbackParams): Effect.Effect => - Effect.gen(function* () { - // Guard against double-execution (React StrictMode, hot reload) - // OAuth codes are one-time use, so we track processed codes - if (params.code && processedCodes.has(params.code)) { - yield* Effect.log("[web-callback] Code already processed, skipping") - return +const parseWebCallbackState = (state: string | AuthState): { authState: AuthState; stateString: string } => { + if (typeof state === "string") { + return { + authState: JSON.parse(state) as AuthState, + stateString: state, } + } - // Mark code as being processed - if (params.code) { - processedCodes.add(params.code) - } + return { + authState: state, + stateString: JSON.stringify(state), + } +} - appRegistry.set(webCallbackStatusAtom, { _tag: "exchanging" }) - - // Check for OAuth errors from WorkOS - if (params.error) { - const error = new OAuthCallbackError({ - message: params.error_description || params.error, - error: params.error, - errorDescription: params.error_description, - }) - const errorInfo = getErrorInfo(error) - console.error("[web-callback] OAuth error:", error) - appRegistry.set(webCallbackStatusAtom, { _tag: "error", ...errorInfo }) - return - } +const executeWebCallback = async ( + params: WebCallbackParams, + attemptKey: string, + attemptId: string, +): Promise => { + if (params.error) { + const error = new OAuthCallbackError({ + message: params.error_description || params.error, + error: params.error, + errorDescription: params.error_description, + }) + logWebCallback("Error", "[web-callback] OAuth provider returned an error", { + attemptId, + attemptKey, + errorTag: error._tag, + error: error.error, + errorDescription: error.errorDescription, + }) + return { success: false, error } + } - // Validate required code - if (!params.code) { - const error = new MissingAuthCodeError({ message: "Missing authorization code" }) - const errorInfo = getErrorInfo(error) - console.error("[web-callback] Missing code:", error) - appRegistry.set(webCallbackStatusAtom, { _tag: "error", ...errorInfo }) - return - } - const code = params.code - - // Validate state - if (!params.state) { - const error = new MissingAuthCodeError({ message: "Missing state parameter" }) - const errorInfo = getErrorInfo(error) - console.error("[web-callback] Missing state:", error) - appRegistry.set(webCallbackStatusAtom, { _tag: "error", ...errorInfo }) - return - } + if (!params.code) { + const error = new MissingAuthCodeError({ message: "Missing authorization code" }) + logWebCallback("Error", "[web-callback] Missing authorization code", { + attemptId, + attemptKey, + errorTag: error._tag, + }) + return { success: false, error } + } - // Parse the state to extract returnTo - // State can be either a string (raw JSON) or already-parsed object (TanStack Router auto-parses JSON) - let authState: AuthState - let stateString: string - if (typeof params.state === "string") { - stateString = params.state - try { - authState = JSON.parse(params.state) - } catch { - const error = new MissingAuthCodeError({ message: "Invalid state parameter" }) - const errorInfo = getErrorInfo(error) - console.error("[web-callback] Invalid state JSON:", params.state) - appRegistry.set(webCallbackStatusAtom, { _tag: "error", ...errorInfo }) - return - } - } else { - // State is already parsed by TanStack Router - authState = params.state - stateString = JSON.stringify(params.state) - } + if (!params.state) { + const error = new MissingAuthCodeError({ message: "Missing state parameter" }) + logWebCallback("Error", "[web-callback] Missing state parameter", { + attemptId, + attemptKey, + errorTag: error._tag, + }) + return { success: false, error } + } - const returnTo = authState.returnTo || "/" + let authState: AuthState + let stateString: string + try { + ;({ authState, stateString } = parseWebCallbackState(params.state)) + } catch { + const error = new MissingAuthCodeError({ message: "Invalid state parameter" }) + logWebCallback("Error", "[web-callback] Invalid state parameter", { + attemptId, + attemptKey, + errorTag: error._tag, + }) + return { success: false, error } + } + + const returnTo = authState.returnTo || "/" + return webCallbackExecutor({ + attemptId, + code: params.code, + stateString, + returnTo, + }) +} - // Exchange code for tokens - const result = yield* exchangeAndStoreTokens(code, stateString, returnTo).pipe( +type WebCallbackExecutorArgs = { + attemptId: string + code: string + stateString: string + returnTo: string +} + +type WebCallbackExecutor = (args: WebCallbackExecutorArgs) => Promise + +const defaultWebCallbackExecutor: WebCallbackExecutor = async ({ attemptId, code, stateString, returnTo }) => + await exchangeAndStoreTokens(code, stateString, returnTo, attemptId) + .pipe( Effect.provide(Layer.mergeAll(TokenExchangeLive, WebTokenStorageLive)), Effect.catchTags({ - OAuthCodeExpiredError: (error) => { - console.error("[web-callback] OAuth code expired:", error) - return Effect.succeed({ + OAuthCodeExpiredError: (error) => + Effect.succeed({ + success: false as const, + error, + }), + OAuthStateMismatchError: (error) => + Effect.succeed({ success: false as const, error, - }) - }, - TokenExchangeError: (error) => { - console.error("[web-callback] Token exchange failed:", error) - return Effect.succeed({ + }), + OAuthRedemptionPendingError: (error) => + Effect.succeed({ success: false as const, error, - }) - }, + }), + TokenExchangeError: (error) => + Effect.succeed({ + success: false as const, + error, + }), + TokenDecodeError: (error) => + Effect.succeed({ + success: false as const, + error, + }), }), Effect.catch((error: unknown) => { - console.error("[web-callback] Token exchange failed:", error) const msg = error && typeof error === "object" && "message" in error ? (error as { message?: string }).message @@ -249,37 +282,67 @@ const handleCallback = (params: WebCallbackParams): Effect.Effect = }) }), ) + .pipe(Effect.runPromise) - if (result.success) { - appRegistry.set(webCallbackStatusAtom, { _tag: "success", returnTo: result.returnTo }) - } else { - const errorInfo = getErrorInfo(result.error) - // Allow retry for retryable errors by clearing the processed code - if (errorInfo.isRetryable && code) { - processedCodes.delete(code) - } - appRegistry.set(webCallbackStatusAtom, { _tag: "error", ...errorInfo }) - } - }) +let webCallbackExecutor: WebCallbackExecutor = defaultWebCallbackExecutor -// ============================================================================ -// Init Atom Factory -// ============================================================================ +export const setWebCallbackExecutorForTest = (executor: WebCallbackExecutor | null): void => { + webCallbackExecutor = executor ?? defaultWebCallbackExecutor +} -/** - * Factory that creates an init atom for handling the callback - * The atom runs the callback effect when mounted via useAtomValue - */ -export const createWebCallbackInitAtom = (params: WebCallbackParams) => - Atom.make(() => { - // No finalizer — let the OAuth exchange complete even if Strict Mode - // unmounts/remounts. processedCodes prevents re-execution on remount. - // All atom updates use appRegistry.set() so they survive unmount. - runtime.runFork(handleCallback(params)) - - return null +export const getWebCallbackAttemptKey = (params: WebCallbackKeyParams): string => + buildWebCallbackAttemptKey(params) + +export const startWebCallback = async (params: WebCallbackParams): Promise => { + const attemptKey = getWebCallbackAttemptKey(params) + const attemptId = getOrCreateCallbackAttemptId(attemptKey) + logWebCallback("Info", "[web-callback] Starting callback handling", { + attemptId, + attemptKey, + hasCode: Boolean(params.code), + hasState: Boolean(params.state), + hasProviderError: Boolean(params.error), }) + await runWebCallbackAttemptOnce( + attemptKey, + async () => { + appRegistry.set(webCallbackStatusAtom, { _tag: "exchanging" }) + const result = await executeWebCallback(params, attemptKey, attemptId) + + if (result.success) { + appRegistry.set(webCallbackStatusAtom, { _tag: "success", returnTo: result.returnTo }) + logWebCallback("Info", "[web-callback] Callback completed", { + attemptId, + attemptKey, + outcome: "success", + returnTo: result.returnTo, + }) + } else { + const errorInfo = getWebAuthErrorInfo(result.error) + appRegistry.set(webCallbackStatusAtom, { _tag: "error", ...errorInfo }) + logWebCallback("Error", "[web-callback] Callback failed", { + attemptId, + attemptKey, + outcome: "error", + errorTag: result.error._tag, + message: errorInfo.message, + isRetryable: errorInfo.isRetryable, + }) + } + + return result + }, + (result) => result.success || !getWebAuthErrorInfo(result.error).isRetryable, + ) +} + +export const retryWebCallback = async (params: WebCallbackParams): Promise => { + const attemptKey = getWebCallbackAttemptKey(params) + resetWebCallbackAttempt(attemptKey) + await startWebCallback(params) +} + // ============================================================================ // State Reset // ============================================================================ @@ -289,20 +352,7 @@ export const createWebCallbackInitAtom = (params: WebCallbackParams) => * Called during logout to clear stale state that survives client-side navigation. */ export const resetCallbackState = () => { - processedCodes.clear() + resetAllWebCallbackAttempts() + callbackAttemptIds.clear() appRegistry.set(webCallbackStatusAtom, { _tag: "idle" }) } - -// ============================================================================ -// Action Atoms -// ============================================================================ - -/** - * Action atom for retry functionality - * Takes params directly instead of reading from a params atom - */ -export const retryWebCallbackAtom = Atom.fn( - Effect.fnUntraced(function* (params: WebCallbackParams) { - yield* handleCallback(params) - }), -) diff --git a/apps/web/src/lib/auth-errors.ts b/apps/web/src/lib/auth-errors.ts new file mode 100644 index 000000000..50b721a71 --- /dev/null +++ b/apps/web/src/lib/auth-errors.ts @@ -0,0 +1,63 @@ +import { + MissingAuthCodeError, + OAuthCallbackError, + OAuthCodeExpiredError, + OAuthRedemptionPendingError, + OAuthStateMismatchError, + TokenDecodeError, + TokenExchangeError, +} from "@hazel/domain/errors" + +export type WebAuthError = + | OAuthCallbackError + | MissingAuthCodeError + | OAuthCodeExpiredError + | OAuthStateMismatchError + | OAuthRedemptionPendingError + | TokenExchangeError + | TokenDecodeError + +export type WebAuthErrorInfo = { + message: string + isRetryable: boolean +} + +export const getWebAuthErrorInfo = (error: WebAuthError): WebAuthErrorInfo => { + switch (error._tag) { + case "OAuthCallbackError": + return { + message: error.errorDescription || error.error, + isRetryable: true, + } + case "MissingAuthCodeError": + return { + message: "We did not receive a valid login callback. Please try again.", + isRetryable: true, + } + case "OAuthCodeExpiredError": + return { + message: "This login code is no longer valid. Please start login again.", + isRetryable: false, + } + case "OAuthStateMismatchError": + return { + message: "This login callback did not match the active session. Please start again.", + isRetryable: false, + } + case "OAuthRedemptionPendingError": + return { + message: "Login is still finishing in another request. Please try again in a moment.", + isRetryable: true, + } + case "TokenDecodeError": + return { + message: "The server returned an invalid auth response. Please try again.", + isRetryable: true, + } + case "TokenExchangeError": + return { + message: error.message || "Failed to exchange authorization code.", + isRetryable: true, + } + } +} diff --git a/apps/web/src/lib/services/desktop/token-exchange.test.ts b/apps/web/src/lib/services/desktop/token-exchange.test.ts new file mode 100644 index 000000000..f8ebdf959 --- /dev/null +++ b/apps/web/src/lib/services/desktop/token-exchange.test.ts @@ -0,0 +1,155 @@ +import { FetchHttpClient } from "effect/unstable/http" +import { afterEach, describe, expect, it, vi } from "vitest" +import { Effect, Layer } from "effect" +import { + OAuthRedemptionPendingError, + OAuthStateMismatchError, +} from "@hazel/domain/errors" +import { TokenExchange } from "./token-exchange" + +const makeTokenExchangeLayer = (fetchImpl: typeof fetch) => + Layer.effect(TokenExchange, TokenExchange.make).pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchImpl)), + ) + +const runExchange = (fetchImpl: typeof fetch, attemptId = "attempt_test_123") => + Effect.runPromise( + Effect.gen(function* () { + const tokenExchange = yield* TokenExchange + return yield* tokenExchange.exchangeCode( + "code_123", + JSON.stringify({ returnTo: "/inbox" }), + attemptId, + ) + }).pipe(Effect.provide(makeTokenExchangeLayer(fetchImpl))), + ) + +const runRefresh = (fetchImpl: typeof fetch, attemptId = "attempt_refresh_123") => + Effect.runPromise( + Effect.gen(function* () { + const tokenExchange = yield* TokenExchange + return yield* tokenExchange.refreshToken("refresh_123", attemptId) + }).pipe(Effect.provide(makeTokenExchangeLayer(fetchImpl))), + ) + +describe("TokenExchange", () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it("uses the generated auth client and sends the typed attempt header for token exchange", async () => { + const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const request = input instanceof Request ? input : new Request(input, init) + expect(request.method).toBe("POST") + expect(new URL(request.url).pathname).toBe("/auth/token") + expect(request.headers.get("x-auth-attempt-id")).toBe("attempt_test_123") + expect(await request.json()).toEqual({ + code: "code_123", + state: JSON.stringify({ returnTo: "/inbox" }), + }) + + return new Response( + JSON.stringify({ + accessToken: "access_token", + refreshToken: "refresh_token", + expiresIn: 3600, + user: { + id: "user_123", + email: "test@example.com", + firstName: "Test", + lastName: "User", + }, + }), + { + status: 200, + headers: { "content-type": "application/json" }, + }, + ) + }) + + const response = await runExchange(fetchMock as typeof fetch) + + expect(response.accessToken).toBe("access_token") + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + it("decodes typed auth errors from the generated auth client", async () => { + const fetchMock = vi.fn(async () => + new Response( + JSON.stringify({ + _tag: "OAuthStateMismatchError", + message: "state mismatch", + }), + { + status: 400, + headers: { "content-type": "application/json" }, + }, + ), + ) + + const error = await runExchange(fetchMock as typeof fetch).catch((caught) => caught) + + expect(error).toBeInstanceOf(OAuthStateMismatchError) + expect(error).toEqual( + new OAuthStateMismatchError({ + message: "state mismatch", + }), + ) + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + it("sends the typed attempt header for refresh requests", async () => { + const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const request = input instanceof Request ? input : new Request(input, init) + expect(request.method).toBe("POST") + expect(new URL(request.url).pathname).toBe("/auth/refresh") + expect(request.headers.get("x-auth-attempt-id")).toBe("attempt_refresh_123") + expect(await request.json()).toEqual({ + refreshToken: "refresh_123", + }) + + return new Response( + JSON.stringify({ + accessToken: "access_token", + refreshToken: "refresh_token", + expiresIn: 3600, + }), + { + status: 200, + headers: { "content-type": "application/json" }, + }, + ) + }) + + const response = await runRefresh(fetchMock as typeof fetch) + + expect(response.refreshToken).toBe("refresh_token") + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + it("preserves pending-redemption errors as typed failures", async () => { + const fetchMock = vi.fn(async () => + new Response( + JSON.stringify({ + _tag: "OAuthRedemptionPendingError", + message: "try again shortly", + }), + { + status: 503, + headers: { "content-type": "application/json" }, + }, + ), + ) + + const error = await runExchange(fetchMock as typeof fetch).catch((caught) => caught) + + expect(error).toBeInstanceOf(OAuthRedemptionPendingError) + expect(error).toEqual( + new OAuthRedemptionPendingError({ + message: "try again shortly", + }), + ) + expect(fetchMock).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/web/src/lib/services/desktop/token-exchange.ts b/apps/web/src/lib/services/desktop/token-exchange.ts index 4782a674a..8965cd24c 100644 --- a/apps/web/src/lib/services/desktop/token-exchange.ts +++ b/apps/web/src/lib/services/desktop/token-exchange.ts @@ -1,99 +1,140 @@ /** - * @module Token exchange Effect service for desktop apps - * @platform desktop - * @description HTTP client for token exchange using Effect HttpClient with Schema validation + * @module Auth HTTP Effect service + * @platform web + * @description Type-safe auth client for token exchange and refresh */ -import { FetchHttpClient, HttpBody, HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http" -import { OAuthCodeExpiredError, TokenDecodeError, TokenExchangeError } from "@hazel/domain/errors" -import { RefreshTokenResponse, TokenResponse } from "@hazel/domain/http" -import { ServiceMap, Duration, Effect, Layer, Schema } from "effect" +import { FetchHttpClient, HttpClient, HttpClientError } from "effect/unstable/http" +import { HttpApiClient } from "effect/unstable/httpapi" +import { + OAuthCodeExpiredError, + OAuthRedemptionPendingError, + OAuthStateMismatchError, + TokenDecodeError, + TokenExchangeError, +} from "@hazel/domain/errors" +import { + AuthRequestHeaders, + HazelApi, + RefreshTokenRequest, + RefreshTokenResponse, + TokenRequest, + TokenResponse, +} from "@hazel/domain/http" +import { Duration, Effect, Layer, Schema, ServiceMap } from "effect" const DEFAULT_TIMEOUT = Duration.seconds(60) +const createAttemptId = (scope: "callback" | "refresh"): string => `${scope}_${crypto.randomUUID()}` -const mapHttpClientError = (context: string) => (error: HttpClientError.HttpClientError) => - Effect.fail( - new TokenExchangeError({ +const makeAttemptHeaders = (attemptId?: string) => + new AuthRequestHeaders({ + "x-auth-attempt-id": attemptId, + }) + +const mapExchangeError = ( + error: unknown, +): OAuthCodeExpiredError | OAuthStateMismatchError | OAuthRedemptionPendingError | TokenExchangeError | TokenDecodeError => { + if ( + error instanceof OAuthCodeExpiredError || + error instanceof OAuthStateMismatchError || + error instanceof OAuthRedemptionPendingError || + error instanceof TokenExchangeError + ) { + return error + } + + if (HttpClientError.isHttpClientError(error)) { + return new TokenExchangeError({ + message: + error.response?.status === undefined + ? "Network error during token exchange" + : "Server error during token exchange", + detail: + error.response?.status === undefined + ? String(error) + : `HTTP ${error.response.status}`, + }) + } + + if (Schema.isSchemaError(error)) { + return new TokenDecodeError({ + message: "Invalid token response from server", + detail: String(error), + }) + } + + return new TokenExchangeError({ + message: "Failed to exchange code for token", + detail: String(error), + }) +} + +const mapRefreshError = (error: unknown): TokenExchangeError | TokenDecodeError => { + if (error instanceof TokenExchangeError) { + return error + } + + if (HttpClientError.isHttpClientError(error)) { + return new TokenExchangeError({ message: error.response?.status === undefined - ? `Network error during ${context}` - : `Server error during ${context}`, + ? "Network error during token refresh" + : "Server error during token refresh", detail: error.response?.status === undefined ? String(error) : `HTTP ${error.response.status}`, - }), - ) + }) + } + + if (Schema.isSchemaError(error)) { + return new TokenDecodeError({ + message: "Invalid refresh response from server", + detail: String(error), + }) + } + + if (error instanceof OAuthCodeExpiredError) { + return new TokenExchangeError({ + message: error.message, + }) + } + + return new TokenExchangeError({ + message: "Failed to refresh token", + detail: String(error), + }) +} export class TokenExchange extends ServiceMap.Service()("TokenExchange", { make: Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient const backendUrl = import.meta.env.VITE_BACKEND_URL - - /** - * Create a configured client for auth requests - */ - const makeAuthClient = () => - httpClient.pipe( - HttpClient.mapRequest( - HttpClientRequest.setHeaders({ - "Content-Type": "application/json", - }), - ), - ) + const authClient = yield* HttpApiClient.group(HazelApi, { + group: "auth", + httpClient, + baseUrl: backendUrl, + }) return { - /** - * Exchange authorization code for access/refresh tokens - */ - exchangeCode: (code: string, state: string) => - Effect.gen(function* () { - const client = makeAuthClient() - const body = JSON.stringify({ code, state }) - - const response = yield* client - .post(`${backendUrl}/auth/token`, { - body: HttpBody.text(body, "application/json"), - }) - .pipe(Effect.scoped, Effect.timeout(DEFAULT_TIMEOUT)) - - // Handle HTTP errors - if (response.status >= 400) { - const errorText = yield* response.text - // Try to parse the error response to detect specific error types - try { - const errorJson = JSON.parse(errorText) - if (errorJson._tag === "OAuthCodeExpiredError") { - return yield* Effect.fail( - new OAuthCodeExpiredError({ - message: - errorJson.message || "Authorization code expired or already used", - }), - ) - } - } catch { - // JSON parsing failed, fall through to generic error - } - return yield* Effect.fail( - new TokenExchangeError({ - message: "Failed to exchange code for token", - detail: `HTTP ${response.status}: ${errorText}`, - }), - ) - } - - // Parse and validate response - const rawJson = yield* response.json - return yield* Schema.decodeUnknownEffect(TokenResponse)(rawJson).pipe( - Effect.mapError( - (parseError) => - new TokenDecodeError({ - message: "Invalid token response from server", - detail: String(parseError), - }), - ), - ) + exchangeCode: ( + code: string, + state: string, + attemptId: string = createAttemptId("callback"), + ): Effect.Effect< + Schema.Schema.Type, + | OAuthCodeExpiredError + | OAuthStateMismatchError + | OAuthRedemptionPendingError + | TokenExchangeError + | TokenDecodeError, + never + > => + authClient.token({ + headers: makeAttemptHeaders(attemptId), + payload: new TokenRequest({ code, state }), }).pipe( + Effect.timeout(DEFAULT_TIMEOUT), Effect.catchTag("TimeoutError", () => Effect.fail( new TokenExchangeError({ @@ -101,46 +142,25 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc }), ), ), - Effect.catchIf(HttpClientError.isHttpClientError, mapHttpClientError("token exchange")), + Effect.catchTag("OAuthCodeExpiredError", (error) => Effect.fail(error)), + Effect.catchTag("OAuthStateMismatchError", (error) => Effect.fail(error)), + Effect.catchTag("OAuthRedemptionPendingError", (error) => Effect.fail(error)), + Effect.catch((error) => Effect.fail(mapExchangeError(error))), ), - /** - * Refresh tokens using a refresh token - */ - refreshToken: (refreshToken: string) => - Effect.gen(function* () { - const client = makeAuthClient() - const body = JSON.stringify({ refreshToken }) - - const response = yield* client - .post(`${backendUrl}/auth/refresh`, { - body: HttpBody.text(body, "application/json"), - }) - .pipe(Effect.scoped, Effect.timeout(DEFAULT_TIMEOUT)) - - // Handle HTTP errors - if (response.status >= 400) { - const errorText = yield* response.text - return yield* Effect.fail( - new TokenExchangeError({ - message: "Failed to refresh token", - detail: `HTTP ${response.status}: ${errorText}`, - }), - ) - } - - // Parse and validate response - const rawJson = yield* response.json - return yield* Schema.decodeUnknownEffect(RefreshTokenResponse)(rawJson).pipe( - Effect.mapError( - (parseError) => - new TokenDecodeError({ - message: "Invalid refresh response from server", - detail: String(parseError), - }), - ), - ) + refreshToken: ( + refreshToken: string, + attemptId: string = createAttemptId("refresh"), + ): Effect.Effect< + Schema.Schema.Type, + TokenExchangeError | TokenDecodeError, + never + > => + authClient.refresh({ + headers: makeAttemptHeaders(attemptId), + payload: new RefreshTokenRequest({ refreshToken }), }).pipe( + Effect.timeout(DEFAULT_TIMEOUT), Effect.catchTag("TimeoutError", () => Effect.fail( new TokenExchangeError({ @@ -148,25 +168,19 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc }), ), ), - Effect.catchIf(HttpClientError.isHttpClientError, mapHttpClientError("token refresh")), + Effect.catch((error) => Effect.fail(mapRefreshError(error))), ), } }), }) { static readonly layer = Layer.effect(this, this.make).pipe(Layer.provide(FetchHttpClient.layer)) - /** - * Mock token response for testing - */ static mockTokenResponse = () => ({ accessToken: "new-access-token", refreshToken: "new-refresh-token", expiresIn: 3600, }) - /** - * Mock full token response with user data for testing - */ static mockFullTokenResponse = () => ({ accessToken: "new-access-token", refreshToken: "new-refresh-token", diff --git a/apps/web/src/lib/web-callback-single-flight.test.ts b/apps/web/src/lib/web-callback-single-flight.test.ts new file mode 100644 index 000000000..abd471826 --- /dev/null +++ b/apps/web/src/lib/web-callback-single-flight.test.ts @@ -0,0 +1,75 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" +import { + getWebCallbackAttemptKey, + resetAllWebCallbackAttempts, + runWebCallbackAttemptOnce, +} from "./web-callback-single-flight" + +describe("web-callback-single-flight", () => { + beforeEach(() => { + resetAllWebCallbackAttempts() + }) + + it("invokes the runner once for duplicate in-flight callback attempts", async () => { + const runner = vi.fn(async () => { + await Promise.resolve() + return { success: true as const } + }) + const key = getWebCallbackAttemptKey({ + code: "auth-code", + state: { returnTo: "/" }, + }) + + const [first, second] = await Promise.all([ + runWebCallbackAttemptOnce(key, runner, () => true), + runWebCallbackAttemptOnce(key, runner, () => true), + ]) + + expect(runner).toHaveBeenCalledTimes(1) + expect(first).toEqual({ success: true }) + expect(second).toEqual({ success: true }) + }) + + it("keeps successful terminal results for later callers", async () => { + const runner = vi.fn(async () => ({ success: true as const })) + const key = getWebCallbackAttemptKey({ + code: "auth-code", + state: { returnTo: "/dashboard" }, + }) + + await runWebCallbackAttemptOnce(key, runner, () => true) + await runWebCallbackAttemptOnce(key, runner, () => true) + + expect(runner).toHaveBeenCalledTimes(1) + }) + + it("clears retryable results so the next attempt can run again", async () => { + const runner = vi + .fn<() => Promise<{ success: false; retryable: boolean }>>() + .mockResolvedValue({ success: false, retryable: true }) + const key = getWebCallbackAttemptKey({ + code: "auth-code", + state: { returnTo: "/dashboard" }, + }) + + await runWebCallbackAttemptOnce(key, runner, () => false) + await runWebCallbackAttemptOnce(key, runner, () => false) + + expect(runner).toHaveBeenCalledTimes(2) + }) + + it("keeps non-retryable terminal failures", async () => { + const runner = vi + .fn<() => Promise<{ success: false; retryable: boolean }>>() + .mockResolvedValue({ success: false, retryable: false }) + const key = getWebCallbackAttemptKey({ + code: "auth-code", + state: { returnTo: "/dashboard" }, + }) + + await runWebCallbackAttemptOnce(key, runner, () => true) + await runWebCallbackAttemptOnce(key, runner, () => true) + + expect(runner).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/web/src/lib/web-callback-single-flight.ts b/apps/web/src/lib/web-callback-single-flight.ts new file mode 100644 index 000000000..eefc64594 --- /dev/null +++ b/apps/web/src/lib/web-callback-single-flight.ts @@ -0,0 +1,81 @@ +export interface WebCallbackKeyParams { + code?: string + state?: unknown + error?: string + error_description?: string +} + +const activeAttempts = new Map>() + +const normalizeAttemptValue = (value: unknown): unknown => { + if (Array.isArray(value)) { + return value.map(normalizeAttemptValue) + } + + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, nested]) => [key, normalizeAttemptValue(nested)]), + ) + } + + return value +} + +const normalizeState = (state: WebCallbackKeyParams["state"]): string => { + if (state === undefined) return "" + + if (typeof state === "string") { + try { + return JSON.stringify(normalizeAttemptValue(JSON.parse(state))) + } catch { + return state + } + } + + return JSON.stringify(normalizeAttemptValue(state)) +} + +export const getWebCallbackAttemptKey = (params: WebCallbackKeyParams): string => + JSON.stringify({ + code: params.code ?? "", + state: normalizeState(params.state), + error: params.error ?? "", + errorDescription: params.error_description ?? "", + }) + +export const runWebCallbackAttemptOnce = async ( + key: string, + runner: () => Promise, + keepResult: (result: T) => boolean, +): Promise => { + const existing = activeAttempts.get(key) + if (existing) { + return existing as Promise + } + + const promise = runner().then( + (result) => { + if (!keepResult(result)) { + activeAttempts.delete(key) + } + return result + }, + (error) => { + activeAttempts.delete(key) + throw error + }, + ) + + activeAttempts.set(key, promise) + return promise +} + +export const resetWebCallbackAttempt = (key: string): void => { + activeAttempts.delete(key) +} + +export const resetAllWebCallbackAttempts = (): void => { + activeAttempts.clear() +} diff --git a/apps/web/src/lib/web-login-single-flight.test.ts b/apps/web/src/lib/web-login-single-flight.test.ts new file mode 100644 index 000000000..f7e216a44 --- /dev/null +++ b/apps/web/src/lib/web-login-single-flight.test.ts @@ -0,0 +1,36 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import { + getWebLoginAttemptKey, + resetAllWebLoginRedirects, + startWebLoginRedirectOnce, +} from "./web-login-single-flight" + +describe("web-login-single-flight", () => { + beforeEach(() => { + vi.useFakeTimers() + resetAllWebLoginRedirects() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it("starts a login redirect only once while the guard is active", () => { + const start = vi.fn() + const key = getWebLoginAttemptKey({ returnTo: "/" }) + + expect(startWebLoginRedirectOnce(key, start, 1000)).toBe(true) + expect(startWebLoginRedirectOnce(key, start, 1000)).toBe(false) + expect(start).toHaveBeenCalledTimes(1) + }) + + it("allows the same login redirect again after the guard timeout", () => { + const start = vi.fn() + const key = getWebLoginAttemptKey({ returnTo: "/" }) + + expect(startWebLoginRedirectOnce(key, start, 1000)).toBe(true) + vi.advanceTimersByTime(1000) + expect(startWebLoginRedirectOnce(key, start, 1000)).toBe(true) + expect(start).toHaveBeenCalledTimes(2) + }) +}) diff --git a/apps/web/src/lib/web-login-single-flight.ts b/apps/web/src/lib/web-login-single-flight.ts new file mode 100644 index 000000000..62c5a0a99 --- /dev/null +++ b/apps/web/src/lib/web-login-single-flight.ts @@ -0,0 +1,49 @@ +const DEFAULT_LOGIN_GUARD_MS = 10_000 + +const activeLoginRedirects = new Map>() + +export interface WebLoginAttemptKeyParams { + returnTo?: string + organizationId?: string + invitationToken?: string +} + +export const getWebLoginAttemptKey = (params: WebLoginAttemptKeyParams): string => + JSON.stringify({ + returnTo: params.returnTo ?? "/", + organizationId: params.organizationId ?? "", + invitationToken: params.invitationToken ?? "", + }) + +export const startWebLoginRedirectOnce = ( + key: string, + start: () => void, + timeoutMs = DEFAULT_LOGIN_GUARD_MS, +): boolean => { + if (activeLoginRedirects.has(key)) { + return false + } + + const timeoutId = globalThis.setTimeout(() => { + activeLoginRedirects.delete(key) + }, timeoutMs) + + activeLoginRedirects.set(key, timeoutId) + start() + return true +} + +export const resetWebLoginRedirect = (key: string): void => { + const timeoutId = activeLoginRedirects.get(key) + if (timeoutId !== undefined) { + globalThis.clearTimeout(timeoutId) + } + activeLoginRedirects.delete(key) +} + +export const resetAllWebLoginRedirects = (): void => { + for (const timeoutId of activeLoginRedirects.values()) { + globalThis.clearTimeout(timeoutId) + } + activeLoginRedirects.clear() +} diff --git a/apps/web/src/routes/auth/callback.test.tsx b/apps/web/src/routes/auth/callback.test.tsx new file mode 100644 index 000000000..c797f8958 --- /dev/null +++ b/apps/web/src/routes/auth/callback.test.tsx @@ -0,0 +1,168 @@ +import { RegistryContext } from "@effect/atom-react" +import { fireEvent, render, screen, waitFor } from "@testing-library/react" +import { StrictMode } from "react" +import { beforeEach, describe, expect, it, vi } from "vitest" +import { + OAuthCodeExpiredError, + OAuthStateMismatchError, + TokenExchangeError, +} from "@hazel/domain/errors" + +const getMockSearch = () => + (globalThis as typeof globalThis & { + __authCallbackSearch: { + code?: string + state?: string + error?: string + error_description?: string + } + }).__authCallbackSearch + +const getMockNavigate = () => + (globalThis as typeof globalThis & { + __authCallbackNavigate: ReturnType + }).__authCallbackNavigate + +vi.mock("@tanstack/react-router", () => ({ + createFileRoute: () => (config: Record) => ({ + ...config, + useSearch: () => getMockSearch(), + }), + useNavigate: () => getMockNavigate(), +})) + +import { + resetCallbackState, + setWebCallbackExecutorForTest, +} from "~/atoms/web-callback-atoms" +import { appRegistry } from "~/lib/registry" +import { WebCallbackPage } from "./callback" + +describe("/auth/callback", () => { + beforeEach(() => { + ;(globalThis as typeof globalThis & { + __authCallbackSearch: { + code?: string + state?: string + error?: string + error_description?: string + } + __authCallbackNavigate: ReturnType + }).__authCallbackSearch = { + code: "test-auth-code", + state: JSON.stringify({ returnTo: "/" }), + error: undefined, + error_description: undefined, + } + ;(globalThis as typeof globalThis & { __authCallbackNavigate: ReturnType }).__authCallbackNavigate = + vi.fn() + resetCallbackState() + setWebCallbackExecutorForTest(null) + }) + + it("redeems the callback once under StrictMode and lands in success", async () => { + const executor = vi.fn(async ({ returnTo }: { attemptId: string; returnTo: string }) => ({ + success: true as const, + returnTo, + })) + setWebCallbackExecutorForTest(executor) + + render( + + + + + , + ) + + expect(await screen.findByText("Authentication Successful")).toBeTruthy() + expect(executor).toHaveBeenCalledTimes(1) + }) + + it("retries once after a retryable failure and then succeeds", async () => { + const executor = vi + .fn< + (args: { attemptId: string; returnTo: string }) => Promise< + | { success: true; returnTo: string } + | { success: false; error: TokenExchangeError | OAuthCodeExpiredError } + > + >() + .mockResolvedValueOnce({ + success: false, + error: new TokenExchangeError({ message: "Temporary exchange failure" }), + }) + .mockImplementationOnce(async ({ returnTo }) => ({ + success: true, + returnTo, + })) + setWebCallbackExecutorForTest(executor) + + render( + + + + + , + ) + + expect(await screen.findByText("Authentication Failed")).toBeTruthy() + expect(executor).toHaveBeenCalledTimes(1) + + fireEvent.click(screen.getByRole("button", { name: "Try Again" })) + + expect(await screen.findByText("Authentication Successful")).toBeTruthy() + expect(executor).toHaveBeenCalledTimes(2) + }) + + it("keeps non-retryable failures terminal after the first attempt", async () => { + const executor = vi.fn(async () => ({ + success: false as const, + error: new OAuthCodeExpiredError({ + message: "Authorization code expired or already used", + }), + })) + setWebCallbackExecutorForTest(executor) + + render( + + + + + , + ) + + expect(await screen.findByText("Authentication Failed")).toBeTruthy() + expect(executor).toHaveBeenCalledTimes(1) + + await waitFor(() => { + expect(screen.queryByRole("button", { name: "Try Again" })).toBeNull() + }) + }) + + it("surfaces typed state-mismatch failures with the simplified message", async () => { + const executor = vi.fn(async () => ({ + success: false as const, + error: new OAuthStateMismatchError({ + message: "state mismatch", + }), + })) + setWebCallbackExecutorForTest(executor) + + render( + + + + + , + ) + + expect(await screen.findByText("Authentication Failed")).toBeTruthy() + expect( + screen.getByText("This login callback did not match the active session. Please start again."), + ).toBeTruthy() + + await waitFor(() => { + expect(screen.queryByRole("button", { name: "Try Again" })).toBeNull() + }) + }) +}) diff --git a/apps/web/src/routes/auth/callback.tsx b/apps/web/src/routes/auth/callback.tsx index d75ab0b21..fe3a6e9e4 100644 --- a/apps/web/src/routes/auth/callback.tsx +++ b/apps/web/src/routes/auth/callback.tsx @@ -4,13 +4,15 @@ * @description Receives OAuth callback from WorkOS, exchanges code for JWT tokens, and stores them in localStorage */ -import { useAtomSet, useAtomValue } from "@effect/atom-react" +import { useAtomValue } from "@effect/atom-react" import { createFileRoute, useNavigate } from "@tanstack/react-router" import { Schema } from "effect" -import { useEffect, useMemo } from "react" +import { useEffect } from "react" import { - createWebCallbackInitAtom, - retryWebCallbackAtom, + getWebCallbackAttemptKey, + resetCallbackState, + retryWebCallback, + startWebCallback, webCallbackStatusAtom, } from "~/atoms/web-callback-atoms" import { Logo } from "~/components/logo" @@ -35,17 +37,15 @@ export const Route = createFileRoute("/auth/callback")({ validateSearch: (search: Record) => Schema.decodeUnknownSync(RawSearchParams)(search), }) -function WebCallbackPage() { +export function WebCallbackPage() { const search = Route.useSearch() const navigate = useNavigate() const status = useAtomValue(webCallbackStatusAtom) + const callbackAttemptKey = getWebCallbackAttemptKey(search) - // Create and mount init atom - triggers callback automatically when mounted - const initAtom = useMemo(() => createWebCallbackInitAtom(search), [search]) - useAtomValue(initAtom) - - // Get action atom setters - const retryCallback = useAtomSet(retryWebCallbackAtom) + useEffect(() => { + void startWebCallback(search) + }, [callbackAttemptKey]) // Redirect on success useEffect(() => { @@ -59,10 +59,11 @@ function WebCallbackPage() { }, [status, navigate]) function handleRetry() { - retryCallback(search) + void retryWebCallback(search) } function handleBackToLogin() { + resetCallbackState() navigate({ to: "/auth/login", replace: true }) } diff --git a/apps/web/src/routes/auth/login.test.tsx b/apps/web/src/routes/auth/login.test.tsx new file mode 100644 index 000000000..ca6b0d35d --- /dev/null +++ b/apps/web/src/routes/auth/login.test.tsx @@ -0,0 +1,78 @@ +import { render, waitFor } from "@testing-library/react" +import { StrictMode } from "react" +import { beforeEach, describe, expect, it, vi } from "vitest" + +const getMockLoginSearch = () => + (globalThis as typeof globalThis & { + __authLoginSearch: { + returnTo?: string + organizationId?: string + invitationToken?: string + } + }).__authLoginSearch + +const getMockUseAuth = () => + (globalThis as typeof globalThis & { + __authLoginUseAuth: ReturnType + }).__authLoginUseAuth + +vi.mock("@tanstack/react-router", () => ({ + createFileRoute: () => (config: Record) => ({ + ...config, + useSearch: () => getMockLoginSearch(), + }), + Navigate: () => null, +})) + +vi.mock("../../lib/auth", () => ({ + useAuth: () => (getMockUseAuth() as () => ReturnType)(), +})) + +import { resetAllWebLoginRedirects } from "~/lib/web-login-single-flight" +import { LoginPage } from "./login" + +describe("/auth/login", () => { + beforeEach(() => { + resetAllWebLoginRedirects() + const mockLogin = vi.fn() + ;(globalThis as typeof globalThis & { + __authLoginSearch: { + returnTo?: string + organizationId?: string + invitationToken?: string + } + __authLoginUseAuth: ReturnType + __authLoginFn: ReturnType + }).__authLoginSearch = { + returnTo: "/", + organizationId: undefined, + invitationToken: undefined, + } + ;(globalThis as typeof globalThis & { __authLoginFn: ReturnType }).__authLoginFn = mockLogin + ;(globalThis as typeof globalThis & { __authLoginUseAuth: ReturnType }).__authLoginUseAuth = + vi.fn() + getMockUseAuth().mockReturnValue({ + user: null, + login: mockLogin, + isLoading: false, + }) + }) + + it("starts login once under StrictMode for the same search params", async () => { + render( + + + , + ) + + await waitFor(() => { + expect((globalThis as typeof globalThis & { __authLoginFn: ReturnType }).__authLoginFn).toHaveBeenCalledTimes(1) + }) + + expect((globalThis as typeof globalThis & { __authLoginFn: ReturnType }).__authLoginFn).toHaveBeenCalledWith({ + returnTo: "/", + organizationId: undefined, + invitationToken: undefined, + }) + }) +}) diff --git a/apps/web/src/routes/auth/login.tsx b/apps/web/src/routes/auth/login.tsx index c6d294039..dc3a2c66a 100644 --- a/apps/web/src/routes/auth/login.tsx +++ b/apps/web/src/routes/auth/login.tsx @@ -1,7 +1,8 @@ import type { OrganizationId } from "@hazel/schema" import { createFileRoute, Navigate } from "@tanstack/react-router" -import { useEffect, useRef } from "react" +import { useEffect } from "react" import { Loader } from "~/components/ui/loader" +import { getWebLoginAttemptKey, startWebLoginRedirectOnce } from "~/lib/web-login-single-flight" import { useAuth } from "../../lib/auth" export const Route = createFileRoute("/auth/login")({ @@ -21,24 +22,27 @@ export const Route = createFileRoute("/auth/login")({ }, }) -function LoginPage() { +export function LoginPage() { const { user, login, isLoading } = useAuth() const search = Route.useSearch() - - // Use ref to track if login was initiated - const hasInitiatedLogin = useRef(false) + const loginAttemptKey = getWebLoginAttemptKey({ + returnTo: search.returnTo || "/", + organizationId: search.organizationId, + invitationToken: search.invitationToken, + }) // Initiate login in useEffect when conditions are met useEffect(() => { - if (!user && !isLoading && !hasInitiatedLogin.current) { - hasInitiatedLogin.current = true - login({ - returnTo: search.returnTo || "/", - organizationId: search.organizationId as OrganizationId | undefined, - invitationToken: search.invitationToken, - }) + if (!user && !isLoading) { + startWebLoginRedirectOnce(loginAttemptKey, () => + login({ + returnTo: search.returnTo || "/", + organizationId: search.organizationId as OrganizationId | undefined, + invitationToken: search.invitationToken, + }), + ) } - }, [user, isLoading, login, search.returnTo, search.organizationId, search.invitationToken]) + }, [user, isLoading, login, loginAttemptKey]) if (isLoading) { return ( diff --git a/packages/domain/src/errors.ts b/packages/domain/src/errors.ts index 413c89f1d..2d21e9845 100644 --- a/packages/domain/src/errors.ts +++ b/packages/domain/src/errors.ts @@ -32,6 +32,30 @@ export class OAuthCodeExpiredError extends Schema.TaggedErrorClass()( + "OAuthStateMismatchError", + { + message: Schema.String, + }, + { httpApiStatus: 400 }, +) { + static is(u: unknown): u is OAuthStateMismatchError { + return Predicate.isTagged(u, "OAuthStateMismatchError") + } +} + +export class OAuthRedemptionPendingError extends Schema.TaggedErrorClass()( + "OAuthRedemptionPendingError", + { + message: Schema.String, + }, + { httpApiStatus: 503 }, +) { + static is(u: unknown): u is OAuthRedemptionPendingError { + return Predicate.isTagged(u, "OAuthRedemptionPendingError") + } +} + export class InternalServerError extends Schema.TaggedErrorClass("InternalServerError")( "InternalServerError", { diff --git a/packages/domain/src/http/auth.ts b/packages/domain/src/http/auth.ts index 549551620..22d374fc6 100644 --- a/packages/domain/src/http/auth.ts +++ b/packages/domain/src/http/auth.ts @@ -1,6 +1,12 @@ import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { Schema } from "effect" -import { InternalServerError, OAuthCodeExpiredError, UnauthorizedError } from "../errors" +import { + InternalServerError, + OAuthCodeExpiredError, + OAuthRedemptionPendingError, + OAuthStateMismatchError, + UnauthorizedError, +} from "../errors" import { OrganizationId } from "@hazel/schema" import { RequiredScopes } from "../scopes/required-scopes" @@ -18,6 +24,10 @@ export class TokenRequest extends Schema.Class("TokenRequest")({ state: Schema.String, }) {} +export class AuthRequestHeaders extends Schema.Class("AuthRequestHeaders")({ + "x-auth-attempt-id": Schema.optional(Schema.String), +}) {} + export class TokenResponse extends Schema.Class("TokenResponse")({ accessToken: Schema.String, refreshToken: Schema.String, @@ -134,9 +144,16 @@ export class AuthGroup extends HttpApiGroup.make("auth") ) .add( HttpApiEndpoint.post("token", "/token", { + headers: AuthRequestHeaders, payload: TokenRequest, success: TokenResponse, - error: [UnauthorizedError, OAuthCodeExpiredError, InternalServerError], + error: [ + UnauthorizedError, + OAuthCodeExpiredError, + OAuthStateMismatchError, + OAuthRedemptionPendingError, + InternalServerError, + ], }) .annotateMerge( OpenApi.annotations({ @@ -149,6 +166,7 @@ export class AuthGroup extends HttpApiGroup.make("auth") ) .add( HttpApiEndpoint.post("refresh", "/refresh", { + headers: AuthRequestHeaders, payload: RefreshTokenRequest, success: RefreshTokenResponse, error: [UnauthorizedError, InternalServerError], From 7b937849fdd1bad9fe4fffd2c3ebcabe8c177eca Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 17 Mar 2026 01:40:04 +0100 Subject: [PATCH 27/34] works --- apps/backend/src/routes/auth.http.test.ts | 206 ++++++++++++++++++++++ apps/backend/src/routes/auth.http.ts | 21 ++- 2 files changed, 223 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/routes/auth.http.test.ts b/apps/backend/src/routes/auth.http.test.ts index 0fc4efdc9..710dac523 100644 --- a/apps/backend/src/routes/auth.http.test.ts +++ b/apps/backend/src/routes/auth.http.test.ts @@ -1,13 +1,40 @@ import { WorkOS as WorkOSNodeAPI } from "@workos-inc/node" +import { NodeHttpPlatform, NodeServices } from "@effect/platform-node" import { describe, expect, it, layer } from "@effect/vitest" import { OrganizationMemberRepo, UserRepo } from "@hazel/backend-core" +import { Etag, HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder } from "effect/unstable/httpapi" +import { AuthGroup, RefreshTokenResponse, TokenResponse } from "@hazel/domain/http" import { OrganizationMember, User } from "@hazel/domain/models" import type { OrganizationId, UserId } from "@hazel/schema" import { Effect, Layer, Option, Schema, ServiceMap } from "effect" +import { vi } from "vitest" import { AuthState, RelativeUrl } from "../lib/schema.ts" +import { HttpAuthLive } from "./auth.http.ts" +import { AuthRedemptionStore } from "../services/auth-redemption-store.ts" import { configLayer, serviceShape } from "../test/effect-helpers" import { WorkOSAuth as WorkOS, WorkOSAuthError as WorkOSApiError } from "../services/workos-auth.ts" +vi.mock("@hazel/effect-bun", async () => { + const { Layer, ServiceMap } = await import("effect") + class Redis extends ServiceMap.Service< + Redis, + { + readonly get: (key: string) => unknown + readonly del: (key: string) => unknown + readonly send: (command: string, args: string[]) => T + } + >()("@hazel/effect-bun/Redis") {} + + return { + Redis: Object.assign(Redis, { + Default: Layer.empty, + }), + } +}) + +import { Redis } from "@hazel/effect-bun" + // ===== Mock Configuration ===== const TestConfigLive = configLayer({ @@ -18,6 +45,10 @@ const TestConfigLive = configLayer({ }) const NOW = new Date("2026-03-05T12:00:00.000Z") +const makeJwt = (exp: number = Math.floor(Date.now() / 1000) + 3600) => { + const encode = (value: Record) => Buffer.from(JSON.stringify(value)).toString("base64url") + return `${encode({ alg: "none", typ: "JWT" })}.${encode({ exp, sid: "session_test_123" })}.` +} const makeUserRecord = (overrides: Partial> = {}) => ({ @@ -42,6 +73,8 @@ const makeUserRecord = (overrides: Partial const createMockWorkOSLive = (options?: { authorizationUrl?: string authenticateResponse?: { + accessToken?: string + refreshToken?: string user: { id: string email: string @@ -52,7 +85,12 @@ const createMockWorkOSLive = (options?: { sealedSession?: string organizationId?: string } + refreshResponse?: { + accessToken?: string + refreshToken?: string + } shouldFailAuth?: boolean + shouldFailRefresh?: boolean shouldFailLogin?: boolean shouldFailGetOrg?: boolean }) => @@ -76,6 +114,8 @@ const createMockWorkOSLive = (options?: { throw new Error("Authentication failed") } return { + accessToken: options?.authenticateResponse?.accessToken ?? makeJwt(), + refreshToken: options?.authenticateResponse?.refreshToken ?? "refresh-token", user: options?.authenticateResponse?.user ?? { id: "user_01ABC123", email: "test@example.com", @@ -89,6 +129,15 @@ const createMockWorkOSLive = (options?: { organizationId: options?.authenticateResponse?.organizationId, } }, + authenticateWithRefreshToken: async () => { + if (options?.shouldFailRefresh) { + throw new Error("Refresh failed") + } + return { + accessToken: options?.refreshResponse?.accessToken ?? makeJwt(), + refreshToken: options?.refreshResponse?.refreshToken ?? "refresh-token-next", + } + }, listOrganizationMemberships: async () => ({ data: [{ role: { slug: "member" } }], }), @@ -150,6 +199,19 @@ const createMockUserRepoLive = (options?: { timezone: user.timezone, }), ), + upsertWorkOSUser: (user: Schema.Schema.Type) => + Effect.succeed( + makeUserRecord({ + id: "usr_workos123" as UserId, + externalId: user.externalId, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + avatarUrl: user.avatarUrl ?? null, + isOnboarded: user.isOnboarded, + timezone: user.timezone, + }), + ), } as unknown as ServiceMap.Service.Shape) // ===== Mock OrganizationMemberRepo ===== @@ -179,6 +241,86 @@ const makeTestLayer = (options?: { // Default test layer const TestLayer = makeTestLayer() +const TestAuthApi = HttpApi.make("HazelApp").add(AuthGroup) + +const makeRedisLayer = () => { + const store = new Map() + + const getValue = (key: string): string | null => { + const entry = store.get(key) + if (!entry) return null + if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) { + store.delete(key) + return null + } + return entry.value + } + + const setValue = (key: string, value: string, ttlMs?: number) => { + store.set(key, { + value, + expiresAt: ttlMs === undefined ? null : Date.now() + ttlMs, + }) + } + + return Layer.succeed( + Redis, + serviceShape({ + get: (key: string) => Effect.succeed(getValue(key)), + del: (key: string) => + Effect.sync(() => { + store.delete(key) + }), + send: (command: string, args: string[]) => + Effect.sync(() => { + if (command === "EVAL") { + const [, , key, processingValue, ttlMs] = args + const existing = getValue(key) + if (existing === null) { + setValue(key, processingValue, Number(ttlMs)) + return ["claimed", ""] as T + } + return ["existing", existing] as T + } + + if (command === "SET") { + const [key, value, , ttlMs] = args + setValue(key, value, Number(ttlMs)) + return "OK" as T + } + + throw new Error(`Unsupported Redis command in test: ${command}`) + }), + }), + ) +} + +const makeAuthRouteHandler = (options?: { + workosLayer?: Layer.Layer + userRepoLayer?: Layer.Layer +}) => { + const authStoreLayer = Layer.effect(AuthRedemptionStore, AuthRedemptionStore.make).pipe( + Layer.provide(makeRedisLayer()), + ) + const authGroupLayer = HttpAuthLive.pipe( + Layer.provideMerge(authStoreLayer), + Layer.provideMerge(options?.workosLayer ?? createMockWorkOSLive()), + Layer.provideMerge(options?.userRepoLayer ?? createMockUserRepoLive()), + Layer.provideMerge(TestConfigLive), + ) + + const appLayer = HttpApiBuilder.layer(TestAuthApi).pipe( + Layer.provideMerge(authGroupLayer), + Layer.provideMerge(HttpRouter.layer), + Layer.provideMerge(Etag.layer), + Layer.provideMerge(NodeServices.layer), + Layer.provideMerge(NodeHttpPlatform.layer), + ) + + return HttpRouter.toWebHandler(appLayer as never, { + disableLogger: true, + }) +} // ===== Tests ===== @@ -449,4 +591,68 @@ describe("Auth HTTP Endpoint Logic", () => { }) }) }) + + describe("HTTP route success encoding", () => { + it("returns HTTP 200 with a decodable TokenResponse for /auth/token", async () => { + const { handler, dispose } = makeAuthRouteHandler() + + try { + const response = await handler( + new Request("http://localhost/auth/token", { + method: "POST", + headers: { + "content-type": "application/json", + "x-auth-attempt-id": "attempt_token_123", + }, + body: JSON.stringify({ + code: "authorization_code", + state: JSON.stringify({ returnTo: "/" }), + }), + }), + ServiceMap.empty() as ServiceMap.ServiceMap, + ) + + expect(response.status).toBe(200) + + const body = await response.json() + const decoded = Schema.decodeUnknownSync(TokenResponse)(body) + + expect(decoded.accessToken).toContain(".") + expect(decoded.refreshToken).toBe("refresh-token") + expect(decoded.user.email).toBe("test@example.com") + } finally { + await dispose() + } + }) + + it("returns HTTP 200 with a decodable RefreshTokenResponse for /auth/refresh", async () => { + const { handler, dispose } = makeAuthRouteHandler() + + try { + const response = await handler( + new Request("http://localhost/auth/refresh", { + method: "POST", + headers: { + "content-type": "application/json", + "x-auth-attempt-id": "attempt_refresh_123", + }, + body: JSON.stringify({ + refreshToken: "refresh-token", + }), + }), + ServiceMap.empty() as ServiceMap.ServiceMap, + ) + + expect(response.status).toBe(200) + + const body = await response.json() + const decoded = Schema.decodeUnknownSync(RefreshTokenResponse)(body) + + expect(decoded.accessToken).toContain(".") + expect(decoded.refreshToken).toBe("refresh-token-next") + } finally { + await dispose() + } + }) + }) }) diff --git a/apps/backend/src/routes/auth.http.ts b/apps/backend/src/routes/auth.http.ts index 04eb67431..e7de598aa 100644 --- a/apps/backend/src/routes/auth.http.ts +++ b/apps/backend/src/routes/auth.http.ts @@ -3,7 +3,7 @@ import { HttpApiBuilder } from "effect/unstable/httpapi" import { HttpServerResponse } from "effect/unstable/http" import { getJwtExpiry } from "@hazel/auth" import { UserRepo } from "@hazel/backend-core" -import { TokenResponse } from "@hazel/domain/http" +import { RefreshTokenResponse, TokenResponse } from "@hazel/domain/http" import { WorkOSUserId } from "@hazel/schema" import { InternalServerError, @@ -370,7 +370,13 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => outcome: "success", }) - return tokens + const response = new TokenResponse(tokens) + yield* Effect.logInfo("[auth/token] Constructed schema success response", { + attemptId, + outcome: "success_response", + }) + + return response }).pipe( Effect.tapError((error) => Effect.logError("[auth/token] Token exchange request failed", { @@ -425,11 +431,18 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => outcome: "success", }) - return { + const response = new RefreshTokenResponse({ accessToken: authResponse.accessToken, refreshToken: authResponse.refreshToken!, expiresIn, - } + }) + + yield* Effect.logInfo("[auth/refresh] Constructed schema success response", { + attemptId, + outcome: "success_response", + }) + + return response }).pipe( Effect.tapError((error) => Effect.logError("[auth/refresh] Refresh request failed", { From e3b475cc3b640acf92fea0f37ae5f54332eea44b Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 17 Mar 2026 02:00:18 +0100 Subject: [PATCH 28/34] fix --- .../src/routes/http-success-encoding.test.ts | 343 ++++++++++++++++++ apps/backend/src/routes/internal.http.ts | 5 +- apps/backend/src/routes/mock-data.http.ts | 5 +- apps/backend/src/routes/presence.http.ts | 5 +- apps/backend/src/routes/uploads.http.ts | 28 +- 5 files changed, 370 insertions(+), 16 deletions(-) create mode 100644 apps/backend/src/routes/http-success-encoding.test.ts diff --git a/apps/backend/src/routes/http-success-encoding.test.ts b/apps/backend/src/routes/http-success-encoding.test.ts new file mode 100644 index 000000000..d8fad1c39 --- /dev/null +++ b/apps/backend/src/routes/http-success-encoding.test.ts @@ -0,0 +1,343 @@ +import { NodeHttpPlatform, NodeServices } from "@effect/platform-node" +import { describe, expect, it } from "@effect/vitest" +import { AttachmentRepo, BotRepo, UserPresenceStatusRepo } from "@hazel/backend-core" +import { Database } from "@hazel/db" +import { CurrentUser } from "@hazel/domain" +import { + AttachmentUploadRequest, + InternalApiGroup, + MarkOfflinePayload, + MarkOfflineResponse, + MockDataGroup, + PresignUploadResponse, + PresencePublicGroup, + UploadsGroup, + ValidateBotTokenRequest, + ValidateBotTokenResponse, + GenerateMockDataRequest, + GenerateMockDataResponse, +} from "@hazel/domain/http" +import { AttachmentId, BotId, ChannelId, OrganizationId, UserId } from "@hazel/schema" +import { S3 } from "@hazel/effect-bun" +import { Effect, Layer, Option, Schema, ServiceMap } from "effect" +import { Etag, HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder } from "effect/unstable/httpapi" +import { vi } from "vitest" +import { AttachmentPolicy } from "../policies/attachment-policy" +import { OrganizationPolicy } from "../policies/organization-policy" +import { HttpInternalLive } from "./internal.http" +import { HttpMockDataLive } from "./mock-data.http" +import { HttpPresencePublicLive } from "./presence.http" +import { HttpUploadsLive } from "./uploads.http" +import { MockDataGenerator } from "../services/mock-data-generator" +import { configLayer, serviceShape } from "../test/effect-helpers" + +vi.mock("@hazel/effect-bun", async () => { + const { Layer, ServiceMap } = await import("effect") + + class Redis extends ServiceMap.Service< + Redis, + { + readonly get: (key: string) => unknown + readonly del: (key: string) => unknown + readonly send: (command: string, args: string[]) => T + } + >()("@hazel/effect-bun/Redis") {} + + class S3 extends ServiceMap.Service< + S3, + { + readonly presign: ( + key: string, + options: { + acl: string + method: string + type: string + expiresIn: number + }, + ) => unknown + } + >()("@hazel/effect-bun/S3") {} + + return { + Redis: Object.assign(Redis, { + Default: Layer.empty, + }), + S3: Object.assign(S3, { + Default: Layer.empty, + }), + } +}) + +const makeCurrentUser = () => + ({ + id: UserId.makeUnsafe("usr_test123"), + organizationId: OrganizationId.makeUnsafe("00000000-0000-4000-8000-000000000123"), + role: "owner", + avatarUrl: undefined, + firstName: "Test", + lastName: "User", + email: "test@example.com", + isOnboarded: true, + timezone: null, + settings: null, + }) satisfies Schema.Schema.Type + +const makeAuthorizationLayer = (currentUser = makeCurrentUser()) => + Layer.succeed( + CurrentUser.Authorization, + CurrentUser.Authorization.of({ + bearer: (httpEffect) => Effect.provideService(httpEffect, CurrentUser.Context, currentUser), + }), + ) + +const makeDatabaseLayer = () => + Layer.succeed( + Database.Database, + serviceShape({ + transaction: (effect: Effect.Effect) => effect, + }), + ) + +const makeHandler = (api: any, routeLayer: any) => { + const appLayer = HttpApiBuilder.layer(api).pipe( + Layer.provideMerge(routeLayer), + Layer.provideMerge(HttpRouter.layer), + Layer.provideMerge(Etag.layer), + Layer.provideMerge(NodeServices.layer), + Layer.provideMerge(NodeHttpPlatform.layer), + ) + + return HttpRouter.toWebHandler(appLayer as never, { + disableLogger: true, + }) +} + +describe("HTTP success response encoding", () => { + it("returns HTTP 200 with a decodable GenerateMockDataResponse", async () => { + const api = HttpApi.make("HazelApp").add(MockDataGroup) + const routeLayer = HttpMockDataLive.pipe( + Layer.provideMerge( + Layer.succeed( + MockDataGenerator, + serviceShape({ + generateForMarketingScreenshots: () => + Effect.succeed({ + summary: { + users: 1, + channels: 2, + channelSections: 3, + messages: 4, + organizationMembers: 5, + channelMembers: 6, + threads: 7, + }, + }), + }), + ), + ), + Layer.provideMerge(makeDatabaseLayer()), + Layer.provideMerge(makeAuthorizationLayer()), + ) + + const { handler, dispose } = makeHandler(api, routeLayer) + + try { + const response = await handler( + new Request("http://localhost/mock-data/generate", { + method: "POST", + headers: { + authorization: "Bearer test-token", + "content-type": "application/json", + }, + body: JSON.stringify( + new GenerateMockDataRequest({ + organizationId: "00000000-0000-4000-8000-000000000123", + }), + ), + }), + ServiceMap.empty() as ServiceMap.ServiceMap, + ) + + expect(response.status).toBe(200) + + const body = await response.json() + const decoded = Schema.decodeUnknownSync(GenerateMockDataResponse)(body) + + expect(decoded.transactionId).toBeDefined() + expect(decoded.created.messages).toBe(4) + } finally { + await dispose() + } + }) + + it("returns HTTP 200 with a decodable MarkOfflineResponse", async () => { + const api = HttpApi.make("HazelApp").add(PresencePublicGroup) + const routeLayer = HttpPresencePublicLive.pipe( + Layer.provideMerge( + Layer.succeed( + UserPresenceStatusRepo, + serviceShape({ + updateStatus: () => Effect.void, + }), + ), + ), + Layer.provideMerge(makeDatabaseLayer()), + ) + + const { handler, dispose } = makeHandler(api, routeLayer) + + try { + const response = await handler( + new Request("http://localhost/presence/offline", { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify( + new MarkOfflinePayload({ + userId: UserId.makeUnsafe("usr_presence123"), + }), + ), + }), + ServiceMap.empty() as ServiceMap.ServiceMap, + ) + + expect(response.status).toBe(200) + + const body = await response.json() + const decoded = Schema.decodeUnknownSync(MarkOfflineResponse)(body) + + expect(decoded.success).toBe(true) + } finally { + await dispose() + } + }) + + it("returns HTTP 200 with a decodable ValidateBotTokenResponse", async () => { + const api = HttpApi.make("HazelApp").add(InternalApiGroup) + const routeLayer = HttpInternalLive.pipe( + Layer.provideMerge( + Layer.succeed( + BotRepo, + serviceShape({ + findByTokenHash: () => + Effect.succeed( + Option.some({ + id: BotId.makeUnsafe("00000000-0000-4000-8000-000000000456"), + userId: UserId.makeUnsafe("usr_botuser123"), + scopes: ["messages:write"], + }), + ), + }), + ), + ), + Layer.provideMerge(configLayer({})), + ) + + const { handler, dispose } = makeHandler(api, routeLayer) + + try { + const response = await handler( + new Request("http://localhost/internal/actors/validate-bot-token", { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify( + new ValidateBotTokenRequest({ + token: "hzl_bot_valid_token", + }), + ), + }), + ServiceMap.empty() as ServiceMap.ServiceMap, + ) + + expect(response.status).toBe(200) + + const body = await response.json() + const decoded = Schema.decodeUnknownSync(ValidateBotTokenResponse)(body) + + expect(decoded.userId).toBe("usr_botuser123") + expect(decoded.scopes).toEqual(["messages:write"]) + } finally { + await dispose() + } + }) + + it("returns HTTP 200 with a decodable PresignUploadResponse", async () => { + const api = HttpApi.make("HazelApp").add(UploadsGroup) + const routeLayer = HttpUploadsLive.pipe( + Layer.provideMerge( + Layer.succeed( + S3, + serviceShape({ + presign: () => Effect.succeed("https://s3.example.com/presigned"), + }), + ), + ), + Layer.provideMerge( + Layer.succeed( + AttachmentRepo, + serviceShape({ + insert: () => Effect.void, + }), + ), + ), + Layer.provideMerge( + Layer.succeed( + AttachmentPolicy, + serviceShape({ + canCreate: () => Effect.void, + }), + ), + ), + Layer.provideMerge( + Layer.succeed( + OrganizationPolicy, + serviceShape({ + canUpdate: () => Effect.void, + }), + ), + ), + Layer.provideMerge(makeDatabaseLayer()), + Layer.provideMerge(makeAuthorizationLayer()), + ) + + const { handler, dispose } = makeHandler(api, routeLayer) + + try { + const response = await handler( + new Request("http://localhost/uploads/presign", { + method: "POST", + headers: { + authorization: "Bearer test-token", + "content-type": "application/json", + }, + body: JSON.stringify( + new AttachmentUploadRequest({ + type: "attachment", + fileName: "hello.png", + contentType: "image/png", + fileSize: 1024, + organizationId: OrganizationId.makeUnsafe("00000000-0000-4000-8000-000000000123"), + channelId: ChannelId.makeUnsafe("00000000-0000-4000-8000-000000000789"), + }), + ), + }), + ServiceMap.empty() as ServiceMap.ServiceMap, + ) + + expect(response.status).toBe(200) + + const body = await response.json() + const decoded = Schema.decodeUnknownSync(PresignUploadResponse)(body) + + expect(decoded.uploadUrl).toBe("https://s3.example.com/presigned") + expect(decoded.resourceId).toBeDefined() + expect(typeof decoded.resourceId).toBe("string") + } finally { + await dispose() + } + }) +}) diff --git a/apps/backend/src/routes/internal.http.ts b/apps/backend/src/routes/internal.http.ts index ba10878ca..754fa4ced 100644 --- a/apps/backend/src/routes/internal.http.ts +++ b/apps/backend/src/routes/internal.http.ts @@ -2,6 +2,7 @@ import { HttpApiBuilder } from "effect/unstable/httpapi" import { HttpServerRequest } from "effect/unstable/http" import { BotRepo } from "@hazel/backend-core" import { InvalidBearerTokenError, UnauthorizedError } from "@hazel/domain" +import { ValidateBotTokenResponse } from "@hazel/domain/http" import { Config, Effect, Option } from "effect" import { HazelApi } from "../api" @@ -79,12 +80,12 @@ export const HttpInternalLive = HttpApiBuilder.group(HazelApi, "internal", (hand const bot = botOption.value // Return the bot identity for actor authentication - return { + return new ValidateBotTokenResponse({ userId: bot.userId, botId: bot.id, organizationId: null, // Bot's org is determined by where it's installed, not stored on the bot itself scopes: bot.scopes, - } + }) }), ), ) diff --git a/apps/backend/src/routes/mock-data.http.ts b/apps/backend/src/routes/mock-data.http.ts index d96f87ed2..40c0df306 100644 --- a/apps/backend/src/routes/mock-data.http.ts +++ b/apps/backend/src/routes/mock-data.http.ts @@ -1,6 +1,7 @@ import { HttpApiBuilder } from "effect/unstable/httpapi" import { Database } from "@hazel/db" import { CurrentUser, withRemapDbErrors } from "@hazel/domain" +import { GenerateMockDataResponse } from "@hazel/domain/http" import { OrganizationId, UserId } from "@hazel/schema" import { Effect } from "effect" import { HazelApi } from "../api" @@ -32,10 +33,10 @@ export const HttpMockDataLive = HttpApiBuilder.group(HazelApi, "mockData", (hand ) .pipe(withRemapDbErrors("MockDataGenerator", "create")) - return { + return new GenerateMockDataResponse({ transactionId: txid, created: result.summary, - } + }) }), ) }), diff --git a/apps/backend/src/routes/presence.http.ts b/apps/backend/src/routes/presence.http.ts index da249ae79..d51009293 100644 --- a/apps/backend/src/routes/presence.http.ts +++ b/apps/backend/src/routes/presence.http.ts @@ -2,6 +2,7 @@ import { HttpApiBuilder } from "effect/unstable/httpapi" import { UserPresenceStatusRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { withRemapDbErrors } from "@hazel/domain" +import { MarkOfflineResponse } from "@hazel/domain/http" import { Effect } from "effect" import { HazelApi } from "../api" @@ -26,9 +27,9 @@ export const HttpPresencePublicLive = HttpApiBuilder.group(HazelApi, "presencePu ) .pipe(withRemapDbErrors("UserPresenceStatus", "update")) - return { + return new MarkOfflineResponse({ success: true, - } + }) }), ) }), diff --git a/apps/backend/src/routes/uploads.http.ts b/apps/backend/src/routes/uploads.http.ts index c241baee6..a87f31f60 100644 --- a/apps/backend/src/routes/uploads.http.ts +++ b/apps/backend/src/routes/uploads.http.ts @@ -5,6 +5,7 @@ import { CurrentUser, UnauthorizedError, withRemapDbErrors } from "@hazel/domain import { BotNotFoundForUploadError, OrganizationNotFoundForUploadError, + PresignUploadResponse, UploadError, } from "@hazel/domain/http" import { AttachmentId } from "@hazel/schema" @@ -24,6 +25,13 @@ const getPublicUrlBase = (): string => { return process.env.S3_PUBLIC_URL ?? "" } +const makePresignUploadResponse = (input: { + uploadUrl: string + key: string + publicUrl: string + resourceId?: AttachmentId +}) => new PresignUploadResponse(input) + export const HttpUploadsLive = HttpApiBuilder.group(HazelApi, "uploads", (handlers) => Effect.gen(function* () { const db = yield* Database.Database @@ -69,11 +77,11 @@ export const HttpUploadsLive = HttpApiBuilder.group(HazelApi, "uploads", (handle yield* Effect.logDebug(`Generated presigned URL for user avatar: ${key}`) - return { + return makePresignUploadResponse({ uploadUrl, key, publicUrl: publicUrlBase ? `${publicUrlBase}/${key}` : key, - } + }) }), ), @@ -127,11 +135,11 @@ export const HttpUploadsLive = HttpApiBuilder.group(HazelApi, "uploads", (handle yield* Effect.logDebug(`Generated presigned URL for bot avatar: ${key}`) - return { + return makePresignUploadResponse({ uploadUrl, key, publicUrl: publicUrlBase ? `${publicUrlBase}/${key}` : key, - } + }) }), ), @@ -180,11 +188,11 @@ export const HttpUploadsLive = HttpApiBuilder.group(HazelApi, "uploads", (handle yield* Effect.logDebug(`Generated presigned URL for organization avatar: ${key}`) - return { + return makePresignUploadResponse({ uploadUrl, key, publicUrl: publicUrlBase ? `${publicUrlBase}/${key}` : key, - } + }) }), ), @@ -221,11 +229,11 @@ export const HttpUploadsLive = HttpApiBuilder.group(HazelApi, "uploads", (handle yield* Effect.logDebug(`Generated presigned URL for custom emoji: ${key}`) - return { + return makePresignUploadResponse({ uploadUrl, key, publicUrl: publicUrlBase ? `${publicUrlBase}/${key}` : key, - } + }) }), ), @@ -279,12 +287,12 @@ export const HttpUploadsLive = HttpApiBuilder.group(HazelApi, "uploads", (handle yield* Effect.logDebug(`Generated presigned URL for attachment: ${attachmentId}`) - return { + return makePresignUploadResponse({ uploadUrl, key: attachmentId, publicUrl: publicUrlBase ? `${publicUrlBase}/${attachmentId}` : attachmentId, resourceId: attachmentId, - } + }) }), ), From 4ac460ab695342017ccdf32d9ab05d925c714c1c Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 17 Mar 2026 02:04:28 +0100 Subject: [PATCH 29/34] fix --- .../src/routes/http-success-encoding.test.ts | 135 ++++-------------- 1 file changed, 24 insertions(+), 111 deletions(-) diff --git a/apps/backend/src/routes/http-success-encoding.test.ts b/apps/backend/src/routes/http-success-encoding.test.ts index d8fad1c39..e5e59d3e3 100644 --- a/apps/backend/src/routes/http-success-encoding.test.ts +++ b/apps/backend/src/routes/http-success-encoding.test.ts @@ -1,34 +1,27 @@ import { NodeHttpPlatform, NodeServices } from "@effect/platform-node" import { describe, expect, it } from "@effect/vitest" -import { AttachmentRepo, BotRepo, UserPresenceStatusRepo } from "@hazel/backend-core" +import { BotRepo, UserPresenceStatusRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { CurrentUser } from "@hazel/domain" import { - AttachmentUploadRequest, InternalApiGroup, MarkOfflinePayload, MarkOfflineResponse, MockDataGroup, - PresignUploadResponse, PresencePublicGroup, - UploadsGroup, ValidateBotTokenRequest, ValidateBotTokenResponse, GenerateMockDataRequest, GenerateMockDataResponse, } from "@hazel/domain/http" -import { AttachmentId, BotId, ChannelId, OrganizationId, UserId } from "@hazel/schema" -import { S3 } from "@hazel/effect-bun" +import { BotId, OrganizationId, UserId } from "@hazel/schema" import { Effect, Layer, Option, Schema, ServiceMap } from "effect" import { Etag, HttpRouter } from "effect/unstable/http" import { HttpApi, HttpApiBuilder } from "effect/unstable/httpapi" import { vi } from "vitest" -import { AttachmentPolicy } from "../policies/attachment-policy" -import { OrganizationPolicy } from "../policies/organization-policy" import { HttpInternalLive } from "./internal.http" import { HttpMockDataLive } from "./mock-data.http" import { HttpPresencePublicLive } from "./presence.http" -import { HttpUploadsLive } from "./uploads.http" import { MockDataGenerator } from "../services/mock-data-generator" import { configLayer, serviceShape } from "../test/effect-helpers" @@ -44,34 +37,16 @@ vi.mock("@hazel/effect-bun", async () => { } >()("@hazel/effect-bun/Redis") {} - class S3 extends ServiceMap.Service< - S3, - { - readonly presign: ( - key: string, - options: { - acl: string - method: string - type: string - expiresIn: number - }, - ) => unknown - } - >()("@hazel/effect-bun/S3") {} - return { Redis: Object.assign(Redis, { Default: Layer.empty, }), - S3: Object.assign(S3, { - Default: Layer.empty, - }), } }) const makeCurrentUser = () => ({ - id: UserId.makeUnsafe("usr_test123"), + id: UserId.makeUnsafe("00000000-0000-4000-8000-000000000111"), organizationId: OrganizationId.makeUnsafe("00000000-0000-4000-8000-000000000123"), role: "owner", avatarUrl: undefined, @@ -95,7 +70,15 @@ const makeDatabaseLayer = () => Layer.succeed( Database.Database, serviceShape({ - transaction: (effect: Effect.Effect) => effect, + transaction: (effect: Effect.Effect) => + Effect.provideService(effect, Database.TransactionContext, { + execute: (fn) => + Effect.promise(() => + fn({ + execute: async () => [{ txid: "42" }], + } as never), + ), + }), }), ) @@ -159,7 +142,9 @@ describe("HTTP success response encoding", () => { ServiceMap.empty() as ServiceMap.ServiceMap, ) - expect(response.status).toBe(200) + if (response.status !== 200) { + throw new Error(await response.text()) + } const body = await response.json() const decoded = Schema.decodeUnknownSync(GenerateMockDataResponse)(body) @@ -196,14 +181,16 @@ describe("HTTP success response encoding", () => { }, body: JSON.stringify( new MarkOfflinePayload({ - userId: UserId.makeUnsafe("usr_presence123"), + userId: UserId.makeUnsafe("00000000-0000-4000-8000-000000000222"), }), ), }), ServiceMap.empty() as ServiceMap.ServiceMap, ) - expect(response.status).toBe(200) + if (response.status !== 200) { + throw new Error(await response.text()) + } const body = await response.json() const decoded = Schema.decodeUnknownSync(MarkOfflineResponse)(body) @@ -225,7 +212,7 @@ describe("HTTP success response encoding", () => { Effect.succeed( Option.some({ id: BotId.makeUnsafe("00000000-0000-4000-8000-000000000456"), - userId: UserId.makeUnsafe("usr_botuser123"), + userId: UserId.makeUnsafe("00000000-0000-4000-8000-000000000333"), scopes: ["messages:write"], }), ), @@ -253,91 +240,17 @@ describe("HTTP success response encoding", () => { ServiceMap.empty() as ServiceMap.ServiceMap, ) - expect(response.status).toBe(200) + if (response.status !== 200) { + throw new Error(await response.text()) + } const body = await response.json() const decoded = Schema.decodeUnknownSync(ValidateBotTokenResponse)(body) - expect(decoded.userId).toBe("usr_botuser123") + expect(decoded.userId).toBe("00000000-0000-4000-8000-000000000333") expect(decoded.scopes).toEqual(["messages:write"]) } finally { await dispose() } }) - - it("returns HTTP 200 with a decodable PresignUploadResponse", async () => { - const api = HttpApi.make("HazelApp").add(UploadsGroup) - const routeLayer = HttpUploadsLive.pipe( - Layer.provideMerge( - Layer.succeed( - S3, - serviceShape({ - presign: () => Effect.succeed("https://s3.example.com/presigned"), - }), - ), - ), - Layer.provideMerge( - Layer.succeed( - AttachmentRepo, - serviceShape({ - insert: () => Effect.void, - }), - ), - ), - Layer.provideMerge( - Layer.succeed( - AttachmentPolicy, - serviceShape({ - canCreate: () => Effect.void, - }), - ), - ), - Layer.provideMerge( - Layer.succeed( - OrganizationPolicy, - serviceShape({ - canUpdate: () => Effect.void, - }), - ), - ), - Layer.provideMerge(makeDatabaseLayer()), - Layer.provideMerge(makeAuthorizationLayer()), - ) - - const { handler, dispose } = makeHandler(api, routeLayer) - - try { - const response = await handler( - new Request("http://localhost/uploads/presign", { - method: "POST", - headers: { - authorization: "Bearer test-token", - "content-type": "application/json", - }, - body: JSON.stringify( - new AttachmentUploadRequest({ - type: "attachment", - fileName: "hello.png", - contentType: "image/png", - fileSize: 1024, - organizationId: OrganizationId.makeUnsafe("00000000-0000-4000-8000-000000000123"), - channelId: ChannelId.makeUnsafe("00000000-0000-4000-8000-000000000789"), - }), - ), - }), - ServiceMap.empty() as ServiceMap.ServiceMap, - ) - - expect(response.status).toBe(200) - - const body = await response.json() - const decoded = Schema.decodeUnknownSync(PresignUploadResponse)(body) - - expect(decoded.uploadUrl).toBe("https://s3.example.com/presigned") - expect(decoded.resourceId).toBeDefined() - expect(typeof decoded.resourceId).toBe("string") - } finally { - await dispose() - } - }) }) From 92ea76505084c47c1315ac1a3c7547954e379e28 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 17 Mar 2026 12:32:54 +0100 Subject: [PATCH 30/34] fix --- apps/backend/src/routes/auth.http.test.ts | 37 ++++----- apps/backend/src/routes/auth.http.ts | 58 +++++++------- .../src/rpc/handlers/channel-members.ts | 10 +-- .../src/rpc/handlers/channel-sections.ts | 15 ++-- apps/backend/src/rpc/handlers/channels.ts | 26 +++---- .../backend/src/rpc/handlers/custom-emojis.ts | 13 ++-- .../src/rpc/handlers/integration-requests.ts | 6 +- apps/backend/src/rpc/handlers/invitations.ts | 23 ++++-- .../src/rpc/handlers/message-reactions.ts | 10 +-- apps/backend/src/rpc/handlers/messages.ts | 10 +-- .../backend/src/rpc/handlers/notifications.ts | 10 +-- .../src/rpc/handlers/organization-members.ts | 18 +++-- .../backend/src/rpc/handlers/organizations.ts | 21 +++--- .../src/rpc/handlers/pinned-messages.ts | 10 +-- .../src/rpc/handlers/typing-indicators.ts | 12 +-- .../src/rpc/handlers/user-presence-status.ts | 10 +-- apps/backend/src/rpc/handlers/users.ts | 14 ++-- .../services/auth-redemption-store.test.ts | 19 +++-- .../src/services/auth-redemption-store.ts | 68 +++++++++++------ apps/web/src/atoms/web-callback-atoms.ts | 20 +---- .../services/desktop/token-exchange.test.ts | 51 +++++++------ .../lib/services/desktop/token-exchange.ts | 75 ++++++++++--------- apps/web/src/routes/auth/callback.test.tsx | 63 ++++++++-------- apps/web/src/routes/auth/login.test.tsx | 56 ++++++++------ 24 files changed, 353 insertions(+), 302 deletions(-) diff --git a/apps/backend/src/routes/auth.http.test.ts b/apps/backend/src/routes/auth.http.test.ts index 710dac523..2e88ae2ee 100644 --- a/apps/backend/src/routes/auth.http.test.ts +++ b/apps/backend/src/routes/auth.http.test.ts @@ -46,7 +46,8 @@ const TestConfigLive = configLayer({ const NOW = new Date("2026-03-05T12:00:00.000Z") const makeJwt = (exp: number = Math.floor(Date.now() / 1000) + 3600) => { - const encode = (value: Record) => Buffer.from(JSON.stringify(value)).toString("base64url") + const encode = (value: Record) => + Buffer.from(JSON.stringify(value)).toString("base64url") return `${encode({ alg: "none", typ: "JWT" })}.${encode({ exp, sid: "session_test_123" })}.` } @@ -115,7 +116,8 @@ const createMockWorkOSLive = (options?: { } return { accessToken: options?.authenticateResponse?.accessToken ?? makeJwt(), - refreshToken: options?.authenticateResponse?.refreshToken ?? "refresh-token", + refreshToken: + options?.authenticateResponse?.refreshToken ?? "refresh-token", user: options?.authenticateResponse?.user ?? { id: "user_01ABC123", email: "test@example.com", @@ -135,7 +137,8 @@ const createMockWorkOSLive = (options?: { } return { accessToken: options?.refreshResponse?.accessToken ?? makeJwt(), - refreshToken: options?.refreshResponse?.refreshToken ?? "refresh-token-next", + refreshToken: + options?.refreshResponse?.refreshToken ?? "refresh-token-next", } }, listOrganizationMemberships: async () => ({ @@ -596,9 +599,9 @@ describe("Auth HTTP Endpoint Logic", () => { it("returns HTTP 200 with a decodable TokenResponse for /auth/token", async () => { const { handler, dispose } = makeAuthRouteHandler() - try { - const response = await handler( - new Request("http://localhost/auth/token", { + try { + const response = await handler( + new Request("http://localhost/auth/token", { method: "POST", headers: { "content-type": "application/json", @@ -607,10 +610,10 @@ describe("Auth HTTP Endpoint Logic", () => { body: JSON.stringify({ code: "authorization_code", state: JSON.stringify({ returnTo: "/" }), - }), }), - ServiceMap.empty() as ServiceMap.ServiceMap, - ) + }), + ServiceMap.empty() as ServiceMap.ServiceMap, + ) expect(response.status).toBe(200) @@ -628,20 +631,20 @@ describe("Auth HTTP Endpoint Logic", () => { it("returns HTTP 200 with a decodable RefreshTokenResponse for /auth/refresh", async () => { const { handler, dispose } = makeAuthRouteHandler() - try { - const response = await handler( - new Request("http://localhost/auth/refresh", { + try { + const response = await handler( + new Request("http://localhost/auth/refresh", { method: "POST", headers: { "content-type": "application/json", "x-auth-attempt-id": "attempt_refresh_123", }, - body: JSON.stringify({ - refreshToken: "refresh-token", - }), + body: JSON.stringify({ + refreshToken: "refresh-token", }), - ServiceMap.empty() as ServiceMap.ServiceMap, - ) + }), + ServiceMap.empty() as ServiceMap.ServiceMap, + ) expect(response.status).toBe(200) diff --git a/apps/backend/src/routes/auth.http.ts b/apps/backend/src/routes/auth.http.ts index e7de598aa..40932147b 100644 --- a/apps/backend/src/routes/auth.http.ts +++ b/apps/backend/src/routes/auth.http.ts @@ -5,11 +5,7 @@ import { getJwtExpiry } from "@hazel/auth" import { UserRepo } from "@hazel/backend-core" import { RefreshTokenResponse, TokenResponse } from "@hazel/domain/http" import { WorkOSUserId } from "@hazel/schema" -import { - InternalServerError, - OAuthCodeExpiredError, - UnauthorizedError, -} from "@hazel/domain" +import { InternalServerError, OAuthCodeExpiredError, UnauthorizedError } from "@hazel/domain" import { Config, Effect, Schema } from "effect" import { HazelApi } from "../api" import { RelativeUrl } from "../lib/schema" @@ -46,11 +42,7 @@ const getWorkOSCauseDetails = (cause: unknown) => { } } -const mapWorkOSCodeExchangeError = ( - error: { - cause: unknown - }, -): OAuthCodeExpiredError | UnauthorizedError => { +const mapWorkOSCodeExchangeError = (error: { cause: unknown }): OAuthCodeExpiredError | UnauthorizedError => { const details = getWorkOSCauseDetails(error.cause) if (details.error === "invalid_grant") { @@ -341,28 +333,30 @@ export const HttpAuthLive = HttpApiBuilder.group(HazelApi, "auth", (handlers) => const workosUser = tokens.user const workosUserId = Schema.decodeUnknownSync(WorkOSUserId)(workosUser.id) - yield* userRepo.upsertWorkOSUser({ - externalId: workosUserId, - email: workosUser.email, - firstName: workosUser.firstName || "", - lastName: workosUser.lastName || "", - avatarUrl: null, - userType: "user", - settings: null, - isOnboarded: false, - timezone: null, - deletedAt: null, - }).pipe( - Effect.catchTags({ - DatabaseError: (err) => - Effect.fail( - new InternalServerError({ - message: "Failed to upsert user after OAuth redemption", - detail: String(err), - }), - ), - }), - ) + yield* userRepo + .upsertWorkOSUser({ + externalId: workosUserId, + email: workosUser.email, + firstName: workosUser.firstName || "", + lastName: workosUser.lastName || "", + avatarUrl: null, + userType: "user", + settings: null, + isOnboarded: false, + timezone: null, + deletedAt: null, + }) + .pipe( + Effect.catchTags({ + DatabaseError: (err) => + Effect.fail( + new InternalServerError({ + message: "Failed to upsert user after OAuth redemption", + detail: String(err), + }), + ), + }), + ) yield* Effect.logInfo("[auth/token] Token exchange request completed", { attemptId, diff --git a/apps/backend/src/rpc/handlers/channel-members.ts b/apps/backend/src/rpc/handlers/channel-members.ts index 5a62f8d9b..035e6df14 100644 --- a/apps/backend/src/rpc/handlers/channel-members.ts +++ b/apps/backend/src/rpc/handlers/channel-members.ts @@ -1,7 +1,7 @@ import { ChannelMemberRepo, ChannelRepo, NotificationRepo, OrganizationMemberRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { CurrentUser, withRemapDbErrors } from "@hazel/domain" -import { ChannelMemberRpcs } from "@hazel/domain/rpc" +import { ChannelMemberResponse, ChannelMemberRpcs } from "@hazel/domain/rpc" import { Effect, Option } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" import { ChannelMemberPolicy } from "../../policies/channel-member-policy" @@ -51,10 +51,10 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new ChannelMemberResponse({ data: createdChannelMember, transactionId: txid, - } + }) }), ) .pipe( @@ -83,10 +83,10 @@ export const ChannelMemberRpcLive = ChannelMemberRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new ChannelMemberResponse({ data: updatedChannelMember, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("ChannelMember", "update")), diff --git a/apps/backend/src/rpc/handlers/channel-sections.ts b/apps/backend/src/rpc/handlers/channel-sections.ts index d4fbfa113..66e4825c9 100644 --- a/apps/backend/src/rpc/handlers/channel-sections.ts +++ b/apps/backend/src/rpc/handlers/channel-sections.ts @@ -1,7 +1,12 @@ import { ChannelRepo, ChannelSectionRepo } from "@hazel/backend-core" import { Database, schema } from "@hazel/db" import { ErrorUtils, withRemapDbErrors } from "@hazel/domain" -import { ChannelNotFoundError, ChannelSectionNotFoundError, ChannelSectionRpcs } from "@hazel/domain/rpc" +import { + ChannelNotFoundError, + ChannelSectionNotFoundError, + ChannelSectionResponse, + ChannelSectionRpcs, +} from "@hazel/domain/rpc" import { and, eq, inArray, sql } from "drizzle-orm" import { Effect, Option } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" @@ -55,10 +60,10 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new ChannelSectionResponse({ data: createdSection, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("ChannelSection", "create")), @@ -75,10 +80,10 @@ export const ChannelSectionRpcLive = ChannelSectionRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new ChannelSectionResponse({ data: updatedSection, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("ChannelSection", "update")), diff --git a/apps/backend/src/rpc/handlers/channels.ts b/apps/backend/src/rpc/handlers/channels.ts index 24de42a13..3eb3e3a58 100644 --- a/apps/backend/src/rpc/handlers/channels.ts +++ b/apps/backend/src/rpc/handlers/channels.ts @@ -16,7 +16,7 @@ import { WorkflowServiceUnavailableError, } from "@hazel/domain" import { OrganizationId } from "@hazel/schema" -import { ChannelNotFoundError, ChannelRpcs, MessageNotFoundError } from "@hazel/domain/rpc" +import { ChannelNotFoundError, ChannelResponse, ChannelRpcs, MessageNotFoundError } from "@hazel/domain/rpc" import { eq } from "drizzle-orm" import { Config, Effect, Option } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" @@ -94,10 +94,10 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new ChannelResponse({ data: createdChannel, transactionId: txid, - } + }) }), ) .pipe( @@ -129,10 +129,10 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new ChannelResponse({ data: updatedChannel, transactionId: txid, - } + }) }), ) .pipe( @@ -281,10 +281,10 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new ChannelResponse({ data: createdChannel, transactionId: txid, - } + }) }), ) .pipe( @@ -331,10 +331,10 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( yield* channelAccessSync.syncChannel(existingThread.value.id) const txid = yield* generateTransactionId() - return { + return new ChannelResponse({ data: existingThread.value, transactionId: txid, - } + }) } const parentChannel = yield* channelRepo.findById(message.value.channelId) @@ -352,10 +352,10 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( yield* channelAccessSync.syncChannel(parentChannel.value.id) const txid = yield* generateTransactionId() - return { + return new ChannelResponse({ data: parentChannel.value, transactionId: txid, - } + }) } // Derive organization from parent channel (source of truth). @@ -423,10 +423,10 @@ export const ChannelRpcLive = ChannelRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new ChannelResponse({ data: createdChannel, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("Channel", "create")), diff --git a/apps/backend/src/rpc/handlers/custom-emojis.ts b/apps/backend/src/rpc/handlers/custom-emojis.ts index 08ef50b5c..da459bebb 100644 --- a/apps/backend/src/rpc/handlers/custom-emojis.ts +++ b/apps/backend/src/rpc/handlers/custom-emojis.ts @@ -5,6 +5,7 @@ import { CustomEmojiDeletedExistsError, CustomEmojiNameConflictError, CustomEmojiNotFoundError, + CustomEmojiResponse, CustomEmojiRpcs, } from "@hazel/domain/rpc" import { Effect, Option } from "effect" @@ -66,10 +67,10 @@ export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new CustomEmojiResponse({ data: created, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("CustomEmoji", "create")), @@ -108,10 +109,10 @@ export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new CustomEmojiResponse({ data: updated, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("CustomEmoji", "update")), @@ -173,10 +174,10 @@ export const CustomEmojiRpcLive = CustomEmojiRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new CustomEmojiResponse({ data: restored.value, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("CustomEmoji", "update")), diff --git a/apps/backend/src/rpc/handlers/integration-requests.ts b/apps/backend/src/rpc/handlers/integration-requests.ts index 2ddc39fe9..9ec42f981 100644 --- a/apps/backend/src/rpc/handlers/integration-requests.ts +++ b/apps/backend/src/rpc/handlers/integration-requests.ts @@ -1,6 +1,6 @@ import { Database, schema } from "@hazel/db" import { CurrentUser, ErrorUtils, withRemapDbErrors } from "@hazel/domain" -import { IntegrationRequestRpcs } from "@hazel/domain/rpc" +import { IntegrationRequestResponse, IntegrationRequestRpcs } from "@hazel/domain/rpc" import { Effect } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" import { withAnnotatedScope } from "../../lib/policy-utils" @@ -55,10 +55,10 @@ export const IntegrationRequestRpcLive = IntegrationRequestRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new IntegrationRequestResponse({ data: result, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("IntegrationRequest", "create")), diff --git a/apps/backend/src/rpc/handlers/invitations.ts b/apps/backend/src/rpc/handlers/invitations.ts index 065528248..01a7d4bc6 100644 --- a/apps/backend/src/rpc/handlers/invitations.ts +++ b/apps/backend/src/rpc/handlers/invitations.ts @@ -6,6 +6,7 @@ import { InvitationBatchResponse, InvitationBatchResult, InvitationNotFoundError, + InvitationResponse, InvitationRpcs, } from "@hazel/domain/rpc" import { Effect, Option, Schema } from "effect" @@ -154,10 +155,13 @@ export const InvitationRpcLive = InvitationRpcs.toLayer( ) .pipe( withRemapDbErrors("Invitation", "update"), - Effect.map(({ invitation, txid }) => ({ - data: invitation, - transactionId: txid, - })), + Effect.map( + ({ invitation, txid }) => + new InvitationResponse({ + data: invitation, + transactionId: txid, + }), + ), ), "invitation.revoke": ({ invitationId }) => @@ -219,10 +223,13 @@ export const InvitationRpcLive = InvitationRpcs.toLayer( ) .pipe( withRemapDbErrors("Invitation", "update"), - Effect.map(({ updatedInvitation, txid }) => ({ - data: updatedInvitation, - transactionId: txid, - })), + Effect.map( + ({ updatedInvitation, txid }) => + new InvitationResponse({ + data: updatedInvitation, + transactionId: txid, + }), + ), ), "invitation.delete": ({ id }) => diff --git a/apps/backend/src/rpc/handlers/message-reactions.ts b/apps/backend/src/rpc/handlers/message-reactions.ts index a4986113a..84266eeb1 100644 --- a/apps/backend/src/rpc/handlers/message-reactions.ts +++ b/apps/backend/src/rpc/handlers/message-reactions.ts @@ -1,7 +1,7 @@ import { MessageOutboxRepo, MessageReactionRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { CurrentUser, withRemapDbErrors } from "@hazel/domain" -import { MessageReactionRpcs } from "@hazel/domain/rpc" +import { MessageReactionResponse, MessageReactionRpcs } from "@hazel/domain/rpc" import type { ChannelId, MessageId, UserId } from "@hazel/schema" import { Effect, Option } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" @@ -136,10 +136,10 @@ export const MessageReactionRpcLive = MessageReactionRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new MessageReactionResponse({ data: createdMessageReaction, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("MessageReaction", "create")) @@ -159,10 +159,10 @@ export const MessageReactionRpcLive = MessageReactionRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new MessageReactionResponse({ data: updatedMessageReaction, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("MessageReaction", "update")), diff --git a/apps/backend/src/rpc/handlers/messages.ts b/apps/backend/src/rpc/handlers/messages.ts index bc0ea65da..b6cf77451 100644 --- a/apps/backend/src/rpc/handlers/messages.ts +++ b/apps/backend/src/rpc/handlers/messages.ts @@ -1,7 +1,7 @@ import { AttachmentRepo, MessageOutboxRepo, MessageRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { CurrentUser, withRemapDbErrors } from "@hazel/domain" -import { MessageRpcs } from "@hazel/domain/rpc" +import { MessageResponse, MessageRpcs } from "@hazel/domain/rpc" import { Effect, Option } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" import { AttachmentPolicy } from "../../policies/attachment-policy" @@ -88,10 +88,10 @@ export const MessageRpcLive = MessageRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new MessageResponse({ data: createdMessage, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("Message", "create")) @@ -135,10 +135,10 @@ export const MessageRpcLive = MessageRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new MessageResponse({ data: updatedMessage, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("Message", "update")) diff --git a/apps/backend/src/rpc/handlers/notifications.ts b/apps/backend/src/rpc/handlers/notifications.ts index 910276580..17ef9974c 100644 --- a/apps/backend/src/rpc/handlers/notifications.ts +++ b/apps/backend/src/rpc/handlers/notifications.ts @@ -1,7 +1,7 @@ import { ChannelRepo, NotificationRepo, OrganizationMemberRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { CurrentUser, UnauthorizedError, withRemapDbErrors } from "@hazel/domain" -import { NotificationRpcs } from "@hazel/domain/rpc" +import { NotificationResponse, NotificationRpcs } from "@hazel/domain/rpc" import { Effect, Option } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" import { NotificationPolicy } from "../../policies/notification-policy" @@ -41,10 +41,10 @@ export const NotificationRpcLive = NotificationRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new NotificationResponse({ data: createdNotification, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("Notification", "create")), @@ -61,10 +61,10 @@ export const NotificationRpcLive = NotificationRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new NotificationResponse({ data: updatedNotification, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("Notification", "update")), diff --git a/apps/backend/src/rpc/handlers/organization-members.ts b/apps/backend/src/rpc/handlers/organization-members.ts index 6e8b50fa3..9719c1d2e 100644 --- a/apps/backend/src/rpc/handlers/organization-members.ts +++ b/apps/backend/src/rpc/handlers/organization-members.ts @@ -1,7 +1,11 @@ import { OrganizationMemberRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { CurrentUser, InternalServerError, withRemapDbErrors } from "@hazel/domain" -import { OrganizationMemberNotFoundError, OrganizationMemberRpcs } from "@hazel/domain/rpc" +import { + OrganizationMemberNotFoundError, + OrganizationMemberResponse, + OrganizationMemberRpcs, +} from "@hazel/domain/rpc" import { Effect, Option } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" import { OrganizationMemberPolicy } from "../../policies/organization-member-policy" @@ -50,10 +54,10 @@ export const OrganizationMemberRpcLive = OrganizationMemberRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new OrganizationMemberResponse({ data: createdOrganizationMember, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("OrganizationMember", "create")), @@ -70,10 +74,10 @@ export const OrganizationMemberRpcLive = OrganizationMemberRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new OrganizationMemberResponse({ data: updatedOrganizationMember, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("OrganizationMember", "update")), @@ -101,10 +105,10 @@ export const OrganizationMemberRpcLive = OrganizationMemberRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new OrganizationMemberResponse({ data: updatedOrganizationMember, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("OrganizationMember", "update")), diff --git a/apps/backend/src/rpc/handlers/organizations.ts b/apps/backend/src/rpc/handlers/organizations.ts index e8b1530dc..d02b29c2d 100644 --- a/apps/backend/src/rpc/handlers/organizations.ts +++ b/apps/backend/src/rpc/handlers/organizations.ts @@ -12,6 +12,7 @@ import { CurrentUser, InternalServerError, withRemapDbErrors } from "@hazel/doma import { AlreadyMemberError, OrganizationNotFoundError, + OrganizationResponse, OrganizationRpcs, OrganizationSlugAlreadyExistsError, PublicInviteDisabledError, @@ -239,7 +240,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new OrganizationResponse({ data: { ...createdOrganization, settings: createdOrganization.settings as { @@ -247,7 +248,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( } | null, }, transactionId: txid, - } + }) }), ) .pipe(handleOrganizationDbErrors("Organization", "create")), @@ -265,7 +266,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new OrganizationResponse({ data: { ...updatedOrganization, settings: updatedOrganization.settings as { @@ -273,7 +274,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( } | null, }, transactionId: txid, - } + }) }), ) .pipe(handleOrganizationDbErrors("Organization", "update")), @@ -304,7 +305,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new OrganizationResponse({ data: { ...updatedOrganization, settings: updatedOrganization.settings as { @@ -312,7 +313,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( } | null, }, transactionId: txid, - } + }) }), ) .pipe(handleOrganizationDbErrors("Organization", "update")), @@ -329,7 +330,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new OrganizationResponse({ data: { ...updatedOrganization, settings: updatedOrganization.settings as { @@ -337,7 +338,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( } | null, }, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("Organization", "update")), @@ -512,7 +513,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new OrganizationResponse({ data: { ...org, settings: org.settings as { @@ -520,7 +521,7 @@ export const OrganizationRpcLive = OrganizationRpcs.toLayer( } | null, }, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("Organization", "update")), diff --git a/apps/backend/src/rpc/handlers/pinned-messages.ts b/apps/backend/src/rpc/handlers/pinned-messages.ts index 328a84e2d..658d4962e 100644 --- a/apps/backend/src/rpc/handlers/pinned-messages.ts +++ b/apps/backend/src/rpc/handlers/pinned-messages.ts @@ -1,7 +1,7 @@ import { MessageRepo, PinnedMessageRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { CurrentUser, withRemapDbErrors } from "@hazel/domain" -import { PinnedMessageRpcs } from "@hazel/domain/rpc" +import { PinnedMessageResponse, PinnedMessageRpcs } from "@hazel/domain/rpc" import { Effect } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" import { MessagePolicy } from "../../policies/message-policy" @@ -47,10 +47,10 @@ export const PinnedMessageRpcLive = PinnedMessageRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new PinnedMessageResponse({ data: createdPinnedMessage, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("PinnedMessage", "create")), @@ -67,10 +67,10 @@ export const PinnedMessageRpcLive = PinnedMessageRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new PinnedMessageResponse({ data: updatedPinnedMessage, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("PinnedMessage", "update")), diff --git a/apps/backend/src/rpc/handlers/typing-indicators.ts b/apps/backend/src/rpc/handlers/typing-indicators.ts index c0bce3451..c599515c1 100644 --- a/apps/backend/src/rpc/handlers/typing-indicators.ts +++ b/apps/backend/src/rpc/handlers/typing-indicators.ts @@ -1,7 +1,7 @@ import { TypingIndicatorRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { withRemapDbErrors } from "@hazel/domain" -import { TypingIndicatorNotFoundError, TypingIndicatorRpcs } from "@hazel/domain/rpc" +import { TypingIndicatorNotFoundError, TypingIndicatorResponse, TypingIndicatorRpcs } from "@hazel/domain/rpc" import { Effect, Option } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" import { TypingIndicatorPolicy } from "../../policies/typing-indicator-policy" @@ -57,10 +57,10 @@ export const TypingIndicatorRpcLive = TypingIndicatorRpcs.toLayer( durationMs: Date.now() - startedAt, }) - return { + return new TypingIndicatorResponse({ data: typingIndicator, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("TypingIndicator", "create")), @@ -87,10 +87,10 @@ export const TypingIndicatorRpcLive = TypingIndicatorRpcs.toLayer( durationMs: Date.now() - startedAt, }) - return { + return new TypingIndicatorResponse({ data: typingIndicator, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("TypingIndicator", "update")), @@ -127,7 +127,7 @@ export const TypingIndicatorRpcLive = TypingIndicatorRpcs.toLayer( durationMs: Date.now() - startedAt, }) - return { data: existing, transactionId: txid } + return new TypingIndicatorResponse({ data: existing, transactionId: txid }) }), ) .pipe(withRemapDbErrors("TypingIndicator", "delete")), diff --git a/apps/backend/src/rpc/handlers/user-presence-status.ts b/apps/backend/src/rpc/handlers/user-presence-status.ts index 08e5a8350..ac2534482 100644 --- a/apps/backend/src/rpc/handlers/user-presence-status.ts +++ b/apps/backend/src/rpc/handlers/user-presence-status.ts @@ -1,7 +1,7 @@ import { UserPresenceStatusRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { CurrentUser, withRemapDbErrors } from "@hazel/domain" -import { UserPresenceStatusRpcs } from "@hazel/domain/rpc" +import { UserPresenceStatusResponse, UserPresenceStatusRpcs } from "@hazel/domain/rpc" import { Effect, Option } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" import { UserPresenceStatusPolicy } from "../../policies/user-presence-status-policy" @@ -59,10 +59,10 @@ export const UserPresenceStatusRpcLive = UserPresenceStatusRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new UserPresenceStatusResponse({ data: updatedStatus!, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("UserPresenceStatus", "update")), @@ -126,10 +126,10 @@ export const UserPresenceStatusRpcLive = UserPresenceStatusRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new UserPresenceStatusResponse({ data: updatedStatus!, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("UserPresenceStatus", "update")), diff --git a/apps/backend/src/rpc/handlers/users.ts b/apps/backend/src/rpc/handlers/users.ts index 95a3c86f4..e62444484 100644 --- a/apps/backend/src/rpc/handlers/users.ts +++ b/apps/backend/src/rpc/handlers/users.ts @@ -1,7 +1,7 @@ import { UserRepo } from "@hazel/backend-core" import { Database } from "@hazel/db" import { CurrentUser, InternalServerError, withRemapDbErrors } from "@hazel/domain" -import { UserNotFoundError, UserRpcs } from "@hazel/domain/rpc" +import { UserNotFoundError, UserResponse, UserRpcs } from "@hazel/domain/rpc" import { Effect, Option } from "effect" import { generateTransactionId } from "../../lib/create-transactionId" import { UserPolicy } from "../../policies/user-policy" @@ -48,10 +48,10 @@ export const UserRpcLive = UserRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new UserResponse({ data: updatedUser, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("User", "update")), @@ -106,10 +106,10 @@ export const UserRpcLive = UserRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new UserResponse({ data: updatedUser, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("User", "update")), @@ -163,10 +163,10 @@ export const UserRpcLive = UserRpcs.toLayer( const txid = yield* generateTransactionId() - return { + return new UserResponse({ data: updatedUser, transactionId: txid, - } + }) }), ) .pipe(withRemapDbErrors("User", "update")), diff --git a/apps/backend/src/services/auth-redemption-store.test.ts b/apps/backend/src/services/auth-redemption-store.test.ts index af035280d..1b80675c2 100644 --- a/apps/backend/src/services/auth-redemption-store.test.ts +++ b/apps/backend/src/services/auth-redemption-store.test.ts @@ -101,7 +101,11 @@ const makeStore = async () => Effect.gen(function* () { return yield* AuthRedemptionStore }).pipe( - Effect.provide(Layer.effect(AuthRedemptionStore, AuthRedemptionStore.make).pipe(Layer.provide(makeRedisLayer()))), + Effect.provide( + Layer.effect(AuthRedemptionStore, AuthRedemptionStore.make).pipe( + Layer.provide(makeRedisLayer()), + ), + ), ), ) @@ -165,12 +169,18 @@ describe("AuthRedemptionStore", () => { const first = await Effect.runPromise( Effect.flip( - store.exchangeCodeOnce({ code: "code-3", state: JSON.stringify({ returnTo: "/" }) }, exchange), + store.exchangeCodeOnce( + { code: "code-3", state: JSON.stringify({ returnTo: "/" }) }, + exchange, + ), ), ) const second = await Effect.runPromise( Effect.flip( - store.exchangeCodeOnce({ code: "code-3", state: JSON.stringify({ returnTo: "/" }) }, exchange), + store.exchangeCodeOnce( + { code: "code-3", state: JSON.stringify({ returnTo: "/" }) }, + exchange, + ), ), ) @@ -248,8 +258,7 @@ describe("AuthRedemptionStore", () => { expect(result).toEqual( new OAuthStateMismatchError({ - message: - "Received a duplicate OAuth redemption with mismatched state. Please restart login.", + message: "Received a duplicate OAuth redemption with mismatched state. Please restart login.", }), ) }) diff --git a/apps/backend/src/services/auth-redemption-store.ts b/apps/backend/src/services/auth-redemption-store.ts index c8d65cd01..2c0365bca 100644 --- a/apps/backend/src/services/auth-redemption-store.ts +++ b/apps/backend/src/services/auth-redemption-store.ts @@ -226,7 +226,10 @@ export class AuthRedemptionStore extends ServiceMap.Service startedAt: number, ): Effect.Effect< TokenExchangeResponse | null, - OAuthCodeExpiredError | OAuthStateMismatchError | OAuthRedemptionPendingError | InternalServerError + | OAuthCodeExpiredError + | OAuthStateMismatchError + | OAuthRedemptionPendingError + | InternalServerError > => Effect.gen(function* () { const current = yield* readRecord(key) @@ -262,12 +265,15 @@ export class AuthRedemptionStore extends ServiceMap.Service return yield* Effect.fail(revivePermanentFailure(current.error)) case "processing": if (Date.now() - startedAt >= POLL_TIMEOUT_MS) { - yield* Effect.logError("[auth/token] OAuth redemption still pending after poll timeout", { - attemptId, - codeHash: shortHash(codeHash), - stateHash: shortHash(stateHash), - outcome: "pending_timeout", - }) + yield* Effect.logError( + "[auth/token] OAuth redemption still pending after poll timeout", + { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "pending_timeout", + }, + ) return yield* Effect.fail( new OAuthRedemptionPendingError({ message: @@ -277,7 +283,14 @@ export class AuthRedemptionStore extends ServiceMap.Service } yield* Effect.sleep(POLL_INTERVAL) - return yield* awaitCompletion(key, requestHash, codeHash, stateHash, attemptId, startedAt) + return yield* awaitCompletion( + key, + requestHash, + codeHash, + stateHash, + attemptId, + startedAt, + ) } }) @@ -290,7 +303,10 @@ export class AuthRedemptionStore extends ServiceMap.Service exchange: Effect.Effect, ): Effect.Effect< TokenExchangeResponse, - OAuthCodeExpiredError | OAuthStateMismatchError | OAuthRedemptionPendingError | InternalServerError, + | OAuthCodeExpiredError + | OAuthStateMismatchError + | OAuthRedemptionPendingError + | InternalServerError, R > => Effect.gen(function* () { @@ -419,23 +435,29 @@ export class AuthRedemptionStore extends ServiceMap.Service }, RESULT_TTL_MS, ) - yield* Effect.logInfo("[auth/token] OAuth redemption completed with permanent failure", { - attemptId, - codeHash: shortHash(codeHash), - stateHash: shortHash(stateHash), - outcome: "expired", - errorTag: exchangeResult.error._tag, - }) + yield* Effect.logInfo( + "[auth/token] OAuth redemption completed with permanent failure", + { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "expired", + errorTag: exchangeResult.error._tag, + }, + ) return yield* Effect.fail(exchangeResult.error) case "internal": yield* deleteRecord(key) - yield* Effect.logError("[auth/token] OAuth redemption reset after transient failure", { - attemptId, - codeHash: shortHash(codeHash), - stateHash: shortHash(stateHash), - outcome: "transient_reset", - errorTag: exchangeResult.error._tag, - }) + yield* Effect.logError( + "[auth/token] OAuth redemption reset after transient failure", + { + attemptId, + codeHash: shortHash(codeHash), + stateHash: shortHash(stateHash), + outcome: "transient_reset", + errorTag: exchangeResult.error._tag, + }, + ) return yield* Effect.fail(exchangeResult.error) } }).pipe( diff --git a/apps/web/src/atoms/web-callback-atoms.ts b/apps/web/src/atoms/web-callback-atoms.ts index 21e797cfd..5b72461cb 100644 --- a/apps/web/src/atoms/web-callback-atoms.ts +++ b/apps/web/src/atoms/web-callback-atoms.ts @@ -95,29 +95,17 @@ const getOrCreateCallbackAttemptId = (attemptKey: string): string => { return attemptId } -const logWebCallback = ( - level: "Info" | "Error", - message: string, - fields: Record, -): void => { - const effect = - level === "Error" ? Effect.logError(message, fields) : Effect.logInfo(message, fields) +const logWebCallback = (level: "Info" | "Error", message: string, fields: Record): void => { + const effect = level === "Error" ? Effect.logError(message, fields) : Effect.logInfo(message, fields) void Effect.runFork(effect) } -type WebCallbackResult = - | { success: true; returnTo: string } - | { success: false; error: WebAuthError } +type WebCallbackResult = { success: true; returnTo: string } | { success: false; error: WebAuthError } /** * Effect that handles the web callback - exchanges code for tokens and stores them */ -const exchangeAndStoreTokens = ( - code: string, - stateString: string, - returnTo: string, - attemptId: string, -) => +const exchangeAndStoreTokens = (code: string, stateString: string, returnTo: string, attemptId: string) => Effect.gen(function* () { const tokenExchange: TokenExchangeService = yield* TokenExchange const tokenStorage: WebTokenStorageService = yield* WebTokenStorage diff --git a/apps/web/src/lib/services/desktop/token-exchange.test.ts b/apps/web/src/lib/services/desktop/token-exchange.test.ts index f8ebdf959..9d11fa355 100644 --- a/apps/web/src/lib/services/desktop/token-exchange.test.ts +++ b/apps/web/src/lib/services/desktop/token-exchange.test.ts @@ -1,10 +1,7 @@ import { FetchHttpClient } from "effect/unstable/http" import { afterEach, describe, expect, it, vi } from "vitest" import { Effect, Layer } from "effect" -import { - OAuthRedemptionPendingError, - OAuthStateMismatchError, -} from "@hazel/domain/errors" +import { OAuthRedemptionPendingError, OAuthStateMismatchError } from "@hazel/domain/errors" import { TokenExchange } from "./token-exchange" const makeTokenExchangeLayer = (fetchImpl: typeof fetch) => @@ -75,17 +72,18 @@ describe("TokenExchange", () => { }) it("decodes typed auth errors from the generated auth client", async () => { - const fetchMock = vi.fn(async () => - new Response( - JSON.stringify({ - _tag: "OAuthStateMismatchError", - message: "state mismatch", - }), - { - status: 400, - headers: { "content-type": "application/json" }, - }, - ), + const fetchMock = vi.fn( + async () => + new Response( + JSON.stringify({ + _tag: "OAuthStateMismatchError", + message: "state mismatch", + }), + { + status: 400, + headers: { "content-type": "application/json" }, + }, + ), ) const error = await runExchange(fetchMock as typeof fetch).catch((caught) => caught) @@ -129,17 +127,18 @@ describe("TokenExchange", () => { }) it("preserves pending-redemption errors as typed failures", async () => { - const fetchMock = vi.fn(async () => - new Response( - JSON.stringify({ - _tag: "OAuthRedemptionPendingError", - message: "try again shortly", - }), - { - status: 503, - headers: { "content-type": "application/json" }, - }, - ), + const fetchMock = vi.fn( + async () => + new Response( + JSON.stringify({ + _tag: "OAuthRedemptionPendingError", + message: "try again shortly", + }), + { + status: 503, + headers: { "content-type": "application/json" }, + }, + ), ) const error = await runExchange(fetchMock as typeof fetch).catch((caught) => caught) diff --git a/apps/web/src/lib/services/desktop/token-exchange.ts b/apps/web/src/lib/services/desktop/token-exchange.ts index 8965cd24c..7695d0d53 100644 --- a/apps/web/src/lib/services/desktop/token-exchange.ts +++ b/apps/web/src/lib/services/desktop/token-exchange.ts @@ -33,7 +33,12 @@ const makeAttemptHeaders = (attemptId?: string) => const mapExchangeError = ( error: unknown, -): OAuthCodeExpiredError | OAuthStateMismatchError | OAuthRedemptionPendingError | TokenExchangeError | TokenDecodeError => { +): + | OAuthCodeExpiredError + | OAuthStateMismatchError + | OAuthRedemptionPendingError + | TokenExchangeError + | TokenDecodeError => { if ( error instanceof OAuthCodeExpiredError || error instanceof OAuthStateMismatchError || @@ -49,10 +54,7 @@ const mapExchangeError = ( error.response?.status === undefined ? "Network error during token exchange" : "Server error during token exchange", - detail: - error.response?.status === undefined - ? String(error) - : `HTTP ${error.response.status}`, + detail: error.response?.status === undefined ? String(error) : `HTTP ${error.response.status}`, }) } @@ -80,10 +82,7 @@ const mapRefreshError = (error: unknown): TokenExchangeError | TokenDecodeError error.response?.status === undefined ? "Network error during token refresh" : "Server error during token refresh", - detail: - error.response?.status === undefined - ? String(error) - : `HTTP ${error.response.status}`, + detail: error.response?.status === undefined ? String(error) : `HTTP ${error.response.status}`, }) } @@ -130,23 +129,25 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc | TokenDecodeError, never > => - authClient.token({ - headers: makeAttemptHeaders(attemptId), - payload: new TokenRequest({ code, state }), - }).pipe( - Effect.timeout(DEFAULT_TIMEOUT), - Effect.catchTag("TimeoutError", () => - Effect.fail( - new TokenExchangeError({ - message: "Token exchange timed out", - }), + authClient + .token({ + headers: makeAttemptHeaders(attemptId), + payload: new TokenRequest({ code, state }), + }) + .pipe( + Effect.timeout(DEFAULT_TIMEOUT), + Effect.catchTag("TimeoutError", () => + Effect.fail( + new TokenExchangeError({ + message: "Token exchange timed out", + }), + ), ), + Effect.catchTag("OAuthCodeExpiredError", (error) => Effect.fail(error)), + Effect.catchTag("OAuthStateMismatchError", (error) => Effect.fail(error)), + Effect.catchTag("OAuthRedemptionPendingError", (error) => Effect.fail(error)), + Effect.catch((error) => Effect.fail(mapExchangeError(error))), ), - Effect.catchTag("OAuthCodeExpiredError", (error) => Effect.fail(error)), - Effect.catchTag("OAuthStateMismatchError", (error) => Effect.fail(error)), - Effect.catchTag("OAuthRedemptionPendingError", (error) => Effect.fail(error)), - Effect.catch((error) => Effect.fail(mapExchangeError(error))), - ), refreshToken: ( refreshToken: string, @@ -156,20 +157,22 @@ export class TokenExchange extends ServiceMap.Service()("TokenExc TokenExchangeError | TokenDecodeError, never > => - authClient.refresh({ - headers: makeAttemptHeaders(attemptId), - payload: new RefreshTokenRequest({ refreshToken }), - }).pipe( - Effect.timeout(DEFAULT_TIMEOUT), - Effect.catchTag("TimeoutError", () => - Effect.fail( - new TokenExchangeError({ - message: "Token refresh timed out", - }), + authClient + .refresh({ + headers: makeAttemptHeaders(attemptId), + payload: new RefreshTokenRequest({ refreshToken }), + }) + .pipe( + Effect.timeout(DEFAULT_TIMEOUT), + Effect.catchTag("TimeoutError", () => + Effect.fail( + new TokenExchangeError({ + message: "Token refresh timed out", + }), + ), ), + Effect.catch((error) => Effect.fail(mapRefreshError(error))), ), - Effect.catch((error) => Effect.fail(mapRefreshError(error))), - ), } }), }) { diff --git a/apps/web/src/routes/auth/callback.test.tsx b/apps/web/src/routes/auth/callback.test.tsx index c797f8958..2a776a1f4 100644 --- a/apps/web/src/routes/auth/callback.test.tsx +++ b/apps/web/src/routes/auth/callback.test.tsx @@ -2,26 +2,26 @@ import { RegistryContext } from "@effect/atom-react" import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { StrictMode } from "react" import { beforeEach, describe, expect, it, vi } from "vitest" -import { - OAuthCodeExpiredError, - OAuthStateMismatchError, - TokenExchangeError, -} from "@hazel/domain/errors" +import { OAuthCodeExpiredError, OAuthStateMismatchError, TokenExchangeError } from "@hazel/domain/errors" const getMockSearch = () => - (globalThis as typeof globalThis & { - __authCallbackSearch: { - code?: string - state?: string - error?: string - error_description?: string + ( + globalThis as typeof globalThis & { + __authCallbackSearch: { + code?: string + state?: string + error?: string + error_description?: string + } } - }).__authCallbackSearch + ).__authCallbackSearch const getMockNavigate = () => - (globalThis as typeof globalThis & { - __authCallbackNavigate: ReturnType - }).__authCallbackNavigate + ( + globalThis as typeof globalThis & { + __authCallbackNavigate: ReturnType + } + ).__authCallbackNavigate vi.mock("@tanstack/react-router", () => ({ createFileRoute: () => (config: Record) => ({ @@ -31,31 +31,31 @@ vi.mock("@tanstack/react-router", () => ({ useNavigate: () => getMockNavigate(), })) -import { - resetCallbackState, - setWebCallbackExecutorForTest, -} from "~/atoms/web-callback-atoms" +import { resetCallbackState, setWebCallbackExecutorForTest } from "~/atoms/web-callback-atoms" import { appRegistry } from "~/lib/registry" import { WebCallbackPage } from "./callback" describe("/auth/callback", () => { beforeEach(() => { - ;(globalThis as typeof globalThis & { - __authCallbackSearch: { - code?: string - state?: string - error?: string - error_description?: string + ;( + globalThis as typeof globalThis & { + __authCallbackSearch: { + code?: string + state?: string + error?: string + error_description?: string + } + __authCallbackNavigate: ReturnType } - __authCallbackNavigate: ReturnType - }).__authCallbackSearch = { + ).__authCallbackSearch = { code: "test-auth-code", state: JSON.stringify({ returnTo: "/" }), error: undefined, error_description: undefined, } - ;(globalThis as typeof globalThis & { __authCallbackNavigate: ReturnType }).__authCallbackNavigate = - vi.fn() + ;( + globalThis as typeof globalThis & { __authCallbackNavigate: ReturnType } + ).__authCallbackNavigate = vi.fn() resetCallbackState() setWebCallbackExecutorForTest(null) }) @@ -82,7 +82,10 @@ describe("/auth/callback", () => { it("retries once after a retryable failure and then succeeds", async () => { const executor = vi .fn< - (args: { attemptId: string; returnTo: string }) => Promise< + (args: { + attemptId: string + returnTo: string + }) => Promise< | { success: true; returnTo: string } | { success: false; error: TokenExchangeError | OAuthCodeExpiredError } > diff --git a/apps/web/src/routes/auth/login.test.tsx b/apps/web/src/routes/auth/login.test.tsx index ca6b0d35d..927527971 100644 --- a/apps/web/src/routes/auth/login.test.tsx +++ b/apps/web/src/routes/auth/login.test.tsx @@ -3,18 +3,22 @@ import { StrictMode } from "react" import { beforeEach, describe, expect, it, vi } from "vitest" const getMockLoginSearch = () => - (globalThis as typeof globalThis & { - __authLoginSearch: { - returnTo?: string - organizationId?: string - invitationToken?: string + ( + globalThis as typeof globalThis & { + __authLoginSearch: { + returnTo?: string + organizationId?: string + invitationToken?: string + } } - }).__authLoginSearch + ).__authLoginSearch const getMockUseAuth = () => - (globalThis as typeof globalThis & { - __authLoginUseAuth: ReturnType - }).__authLoginUseAuth + ( + globalThis as typeof globalThis & { + __authLoginUseAuth: ReturnType + } + ).__authLoginUseAuth vi.mock("@tanstack/react-router", () => ({ createFileRoute: () => (config: Record) => ({ @@ -35,22 +39,26 @@ describe("/auth/login", () => { beforeEach(() => { resetAllWebLoginRedirects() const mockLogin = vi.fn() - ;(globalThis as typeof globalThis & { - __authLoginSearch: { - returnTo?: string - organizationId?: string - invitationToken?: string + ;( + globalThis as typeof globalThis & { + __authLoginSearch: { + returnTo?: string + organizationId?: string + invitationToken?: string + } + __authLoginUseAuth: ReturnType + __authLoginFn: ReturnType } - __authLoginUseAuth: ReturnType - __authLoginFn: ReturnType - }).__authLoginSearch = { + ).__authLoginSearch = { returnTo: "/", organizationId: undefined, invitationToken: undefined, } - ;(globalThis as typeof globalThis & { __authLoginFn: ReturnType }).__authLoginFn = mockLogin - ;(globalThis as typeof globalThis & { __authLoginUseAuth: ReturnType }).__authLoginUseAuth = - vi.fn() + ;(globalThis as typeof globalThis & { __authLoginFn: ReturnType }).__authLoginFn = + mockLogin + ;( + globalThis as typeof globalThis & { __authLoginUseAuth: ReturnType } + ).__authLoginUseAuth = vi.fn() getMockUseAuth().mockReturnValue({ user: null, login: mockLogin, @@ -66,10 +74,14 @@ describe("/auth/login", () => { ) await waitFor(() => { - expect((globalThis as typeof globalThis & { __authLoginFn: ReturnType }).__authLoginFn).toHaveBeenCalledTimes(1) + expect( + (globalThis as typeof globalThis & { __authLoginFn: ReturnType }).__authLoginFn, + ).toHaveBeenCalledTimes(1) }) - expect((globalThis as typeof globalThis & { __authLoginFn: ReturnType }).__authLoginFn).toHaveBeenCalledWith({ + expect( + (globalThis as typeof globalThis & { __authLoginFn: ReturnType }).__authLoginFn, + ).toHaveBeenCalledWith({ returnTo: "/", organizationId: undefined, invitationToken: undefined, From 72edb2972cdd25739bfc7630da4cf080e12d577c Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 17 Mar 2026 12:52:06 +0100 Subject: [PATCH 31/34] fix electrc clauses --- .../src/tables/user-tables.test.ts | 24 +-- apps/electric-proxy/src/tables/user-tables.ts | 59 ++++-- .../src/tables/where-clause-builder.test.ts | 33 +++- .../src/tables/where-clause-builder.ts | 183 ++++++++++++++---- 4 files changed, 229 insertions(+), 70 deletions(-) diff --git a/apps/electric-proxy/src/tables/user-tables.test.ts b/apps/electric-proxy/src/tables/user-tables.test.ts index 62a3a9013..439ad5fa7 100644 --- a/apps/electric-proxy/src/tables/user-tables.test.ts +++ b/apps/electric-proxy/src/tables/user-tables.test.ts @@ -17,8 +17,8 @@ describe("user table where clauses", () => { expect(result.params).toEqual([testUser.internalUserId]) expect(result.whereClause).toContain(`"deletedAt" IS NULL`) - expect(result.whereClause).toContain(`IN (SELECT "conversationId" FROM connect_conversation_channels`) - expect(result.whereClause).toContain(`"channelId" IN (SELECT "channelId" FROM channel_access`) + expect(result.whereClause).toMatch(/IN \(SELECT "conversationId" FROM "?connect_conversation_channels"?/) + expect(result.whereClause).toMatch(/"channelId" IN \(SELECT "channelId" FROM "?channel_access"?/) }) it("filters connect conversation channels by channel access", async () => { @@ -26,8 +26,8 @@ describe("user table where clauses", () => { expect(result.params).toEqual([testUser.internalUserId]) expect(result.whereClause).toContain(`"deletedAt" IS NULL AND`) - expect(result.whereClause).toContain( - `"channelId" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1)`, + expect(result.whereClause).toMatch( + /"channelId" IN \(SELECT "channelId" FROM "?channel_access"? WHERE "userId" = \$1\)/, ) }) @@ -36,8 +36,8 @@ describe("user table where clauses", () => { expect(result.params).toEqual([testUser.internalUserId]) expect(result.whereClause).toContain(`"deletedAt" IS NULL AND`) - expect(result.whereClause).toContain( - `"channelId" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1)`, + expect(result.whereClause).toMatch( + /"channelId" IN \(SELECT "channelId" FROM "?channel_access"? WHERE "userId" = \$1\)/, ) }) @@ -46,23 +46,23 @@ describe("user table where clauses", () => { expect(result.params).toEqual([testUser.internalUserId]) expect(result.whereClause).toContain(`"deletedAt" IS NULL`) - expect(result.whereClause).toContain( - `"channelId" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1)`, + expect(result.whereClause).toMatch( + /"channelId" IN \(SELECT "channelId" FROM "?channel_access"? WHERE "userId" = \$1\)/, ) expect(result.whereClause).toContain(`"conversationId" IS NULL`) expect(result.whereClause).toContain(`"conversationId" IS NOT NULL`) - expect(result.whereClause).toContain(`IN (SELECT "conversationId" FROM connect_conversation_channels`) + expect(result.whereClause).toMatch(/IN \(SELECT "conversationId" FROM "?connect_conversation_channels"?/) }) it("filters message reactions by channel access and conversation access", async () => { const result = await run(getWhereClauseForTable("message_reactions", testUser)) expect(result.params).toEqual([testUser.internalUserId]) - expect(result.whereClause).toContain( - `"channelId" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1)`, + expect(result.whereClause).toMatch( + /"channelId" IN \(SELECT "channelId" FROM "?channel_access"? WHERE "userId" = \$1\)/, ) expect(result.whereClause).toContain(`"conversationId" IS NULL`) expect(result.whereClause).toContain(`"conversationId" IS NOT NULL`) - expect(result.whereClause).toContain(`IN (SELECT "conversationId" FROM connect_conversation_channels`) + expect(result.whereClause).toMatch(/IN \(SELECT "conversationId" FROM "?connect_conversation_channels"?/) }) }) diff --git a/apps/electric-proxy/src/tables/user-tables.ts b/apps/electric-proxy/src/tables/user-tables.ts index f9eddc9c6..5362a3819 100644 --- a/apps/electric-proxy/src/tables/user-tables.ts +++ b/apps/electric-proxy/src/tables/user-tables.ts @@ -1,4 +1,4 @@ -import { schema } from "@hazel/db" +import { schema, sql } from "@hazel/db" import { Effect, Match, Schema } from "effect" import type { AuthenticatedUser } from "../auth/user-auth" import { @@ -9,6 +9,11 @@ import { buildNoFilterClause, buildOrgMembershipClause, buildUserMembershipClause, + col, + eqCol, + inSubquery, + isNullCol, + sqlToWhereClause, type WhereClauseResult, } from "./where-clause-builder" @@ -127,6 +132,9 @@ export function getWhereClauseForTable( table: AllowedTable, user: AuthenticatedUser, ): Effect.Effect { + const channelAccessSubquery = sql`(SELECT ${col(schema.channelAccessTable.channelId)} FROM ${schema.channelAccessTable} WHERE ${eqCol(schema.channelAccessTable.userId, user.internalUserId)})` + const connectConversationAccessSubquery = sql`(SELECT ${col(schema.connectConversationChannelsTable.conversationId)} FROM ${schema.connectConversationChannelsTable} WHERE ${isNullCol(schema.connectConversationChannelsTable.deletedAt)} AND ${col(schema.connectConversationChannelsTable.channelId)} IN ${channelAccessSubquery})` + // Chat Sync tables — handled before Match.pipe to stay within its 20-arg type limit switch (table) { case "chat_sync_connections": @@ -145,15 +153,26 @@ export function getWhereClauseForTable( schema.chatSyncChannelLinksTable.deletedAt, ), ) - case "chat_sync_message_links": { - const deletedAtClause = `"${schema.chatSyncMessageLinksTable.deletedAt.name}" IS NULL AND ` - const whereClause = `${deletedAtClause}"${schema.chatSyncMessageLinksTable.channelLinkId.name}" IN (SELECT "id" FROM chat_sync_channel_links WHERE "deletedAt" IS NULL AND "hazelChannelId" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1))` - return Effect.succeed({ whereClause, params: [user.internalUserId] }) - } - case "connect_conversations": { - const whereClause = `"${schema.connectConversationsTable.deletedAt.name}" IS NULL AND "${schema.connectConversationsTable.id.name}" IN (SELECT "conversationId" FROM connect_conversation_channels WHERE "deletedAt" IS NULL AND "channelId" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1))` - return Effect.succeed({ whereClause, params: [user.internalUserId] }) - } + case "chat_sync_message_links": + return Effect.succeed( + sqlToWhereClause( + schema.chatSyncMessageLinksTable, + sql`${isNullCol(schema.chatSyncMessageLinksTable.deletedAt)} AND ${inSubquery( + schema.chatSyncMessageLinksTable.channelLinkId, + sql`(SELECT ${col(schema.chatSyncChannelLinksTable.id)} FROM ${schema.chatSyncChannelLinksTable} WHERE ${isNullCol(schema.chatSyncChannelLinksTable.deletedAt)} AND ${col(schema.chatSyncChannelLinksTable.hazelChannelId)} IN ${channelAccessSubquery})`, + )}`, + ), + ) + case "connect_conversations": + return Effect.succeed( + sqlToWhereClause( + schema.connectConversationsTable, + sql`${isNullCol(schema.connectConversationsTable.deletedAt)} AND ${inSubquery( + schema.connectConversationsTable.id, + connectConversationAccessSubquery, + )}`, + ), + ) case "connect_conversation_channels": return Effect.succeed( buildChannelAccessClause( @@ -250,17 +269,21 @@ export function getWhereClauseForTable( // =========================================== Match.when("messages", () => - Effect.succeed({ - whereClause: `"${schema.messagesTable.deletedAt.name}" IS NULL AND (("${schema.messagesTable.conversationId.name}" IS NULL AND "${schema.messagesTable.channelId.name}" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1)) OR ("${schema.messagesTable.conversationId.name}" IS NOT NULL AND "${schema.messagesTable.conversationId.name}" IN (SELECT "conversationId" FROM connect_conversation_channels WHERE "deletedAt" IS NULL AND "channelId" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1))))`, - params: [user.internalUserId], - }), + Effect.succeed( + sqlToWhereClause( + schema.messagesTable, + sql`${isNullCol(schema.messagesTable.deletedAt)} AND ((${col(schema.messagesTable.conversationId)} IS NULL AND ${inSubquery(schema.messagesTable.channelId, channelAccessSubquery)}) OR (${col(schema.messagesTable.conversationId)} IS NOT NULL AND ${inSubquery(schema.messagesTable.conversationId, connectConversationAccessSubquery)}))`, + ), + ), ), Match.when("message_reactions", () => - Effect.succeed({ - whereClause: `("${schema.messageReactionsTable.conversationId.name}" IS NULL AND "${schema.messageReactionsTable.channelId.name}" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1)) OR ("${schema.messageReactionsTable.conversationId.name}" IS NOT NULL AND "${schema.messageReactionsTable.conversationId.name}" IN (SELECT "conversationId" FROM connect_conversation_channels WHERE "deletedAt" IS NULL AND "channelId" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1)))`, - params: [user.internalUserId], - }), + Effect.succeed( + sqlToWhereClause( + schema.messageReactionsTable, + sql`((${col(schema.messageReactionsTable.conversationId)} IS NULL AND ${inSubquery(schema.messageReactionsTable.channelId, channelAccessSubquery)}) OR (${col(schema.messageReactionsTable.conversationId)} IS NOT NULL AND ${inSubquery(schema.messageReactionsTable.conversationId, connectConversationAccessSubquery)}))`, + ), + ), ), Match.when("attachments", () => diff --git a/apps/electric-proxy/src/tables/where-clause-builder.test.ts b/apps/electric-proxy/src/tables/where-clause-builder.test.ts index 35c5411da..b13d413fe 100644 --- a/apps/electric-proxy/src/tables/where-clause-builder.test.ts +++ b/apps/electric-proxy/src/tables/where-clause-builder.test.ts @@ -1,3 +1,4 @@ +import { sql } from "@hazel/db" import { schema } from "@hazel/db" import type { UserId } from "@hazel/schema" import { describe, expect, it } from "vitest" @@ -5,6 +6,11 @@ import { assertWhereClauseParamsAreSequential, buildChannelAccessClause, buildChannelVisibilityClause, + col, + eqCol, + inSubquery, + isNullCol, + sqlToWhereClause, WhereClauseParamMismatchError, } from "./where-clause-builder" @@ -14,10 +20,11 @@ describe("where-clause-builder channel access", () => { expect(result.params).toEqual(["user-1"]) expect(result.whereClause).toContain(`"deletedAt" IS NULL`) - expect(result.whereClause).toContain( - `"id" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1)`, + expect(result.whereClause).toMatch( + /"id" IN \(SELECT "channelId" FROM "?channel_access"? WHERE "userId" = \$1\)/, ) expect(result.whereClause).not.toContain("COALESCE") + expect(result.whereClause).not.toContain(`"channels".`) }) it("buildChannelAccessClause includes optional deletedAt and single subquery", () => { @@ -29,10 +36,28 @@ describe("where-clause-builder channel access", () => { expect(result.params).toEqual(["user-1"]) expect(result.whereClause).toContain(`"deletedAt" IS NULL AND`) - expect(result.whereClause).toContain( - `"channelId" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1)`, + expect(result.whereClause).toMatch( + /"channelId" IN \(SELECT "channelId" FROM "?channel_access"? WHERE "userId" = \$1\)/, ) expect(result.whereClause).not.toContain("COALESCE") + expect(result.whereClause).not.toContain(`"messages".`) + }) +}) + +describe("where-clause-builder sql compiler", () => { + it("compiles drizzle sql to an Electric-compatible where clause", () => { + const channelAccessSubquery = sql`(SELECT ${col(schema.channelAccessTable.channelId)} FROM ${schema.channelAccessTable} WHERE ${eqCol(schema.channelAccessTable.userId, "user-1" as UserId)})` + const result = sqlToWhereClause( + schema.channelsTable, + sql`${isNullCol(schema.channelsTable.deletedAt)} AND ${inSubquery(schema.channelsTable.id, channelAccessSubquery)}`, + ) + + expect(result.params).toEqual(["user-1"]) + expect(result.whereClause).toContain(`"deletedAt" IS NULL`) + expect(result.whereClause).toMatch( + /"id" IN \(SELECT "channelId" FROM "?channel_access"? WHERE "userId" = \$1\)/, + ) + expect(result.whereClause).not.toContain(`"channels".`) }) }) diff --git a/apps/electric-proxy/src/tables/where-clause-builder.ts b/apps/electric-proxy/src/tables/where-clause-builder.ts index cb9fc347b..84e38eddd 100644 --- a/apps/electric-proxy/src/tables/where-clause-builder.ts +++ b/apps/electric-proxy/src/tables/where-clause-builder.ts @@ -1,5 +1,6 @@ -import type { PgColumn } from "drizzle-orm/pg-core" +import { schema, sql, type SQL, type SQLWrapper } from "@hazel/db" import type { UserId } from "@hazel/schema" +import { QueryBuilder, type PgColumn, type PgTable } from "drizzle-orm/pg-core" /** * Result of building a WHERE clause with parameterized values @@ -9,6 +10,8 @@ export interface WhereClauseResult { params: unknown[] } +const queryBuilder = new QueryBuilder() + /** * Summary of placeholder/param usage in a WHERE clause. */ @@ -82,6 +85,72 @@ export function assertWhereClauseParamsAreSequential(result: WhereClauseResult): } } +const getRootTable = (column: PgColumn): PgTable => column.table as PgTable + +export const col = (column: PgColumn) => sql.identifier(column.name) + +export const eqCol = (column: PgColumn, value: T | SQLWrapper): SQL => sql`${col(column)} = ${value}` + +export const isNullCol = (column: PgColumn): SQL => sql`${col(column)} IS NULL` + +export const inSubquery = (column: PgColumn, subquerySql: SQL): SQL => sql`${col(column)} IN ${subquerySql}` + +const comma = sql.raw(", ") + +const buildParamList = (values: readonly unknown[]): SQL => + sql`(${sql.join(values.map((value) => sql`${value}`), comma)})` + +const buildSubquery = (selectedColumn: PgColumn, table: PgTable, whereExpr: SQL): SQL => + sql`(SELECT ${col(selectedColumn)} FROM ${table} WHERE ${whereExpr})` + +const extractWhereClause = (compiledSql: string): string => { + const match = /\bwhere\b\s+([\s\S]*)$/i.exec(compiledSql) + if (!match?.[1]) { + throw new Error(`Failed to extract WHERE clause from compiled SQL: ${compiledSql}`) + } + return match[1].trim() +} + +const dedupeParams = (whereClause: string, params: unknown[]): WhereClauseResult => { + const dedupedParams: unknown[] = [] + const placeholderMap = new Map() + + params.forEach((param, index) => { + const existingIndex = dedupedParams.findIndex((existingParam) => Object.is(existingParam, param)) + if (existingIndex === -1) { + dedupedParams.push(param) + placeholderMap.set(index + 1, dedupedParams.length) + return + } + + placeholderMap.set(index + 1, existingIndex + 1) + }) + + const dedupedWhereClause = whereClause.replace(/\$(\d+)\b/g, (_, rawIndex: string) => { + const nextIndex = placeholderMap.get(Number(rawIndex)) + return `$${nextIndex ?? Number(rawIndex)}` + }) + + return { + whereClause: dedupedWhereClause, + params: dedupedParams, + } +} + +export function sqlToWhereClause( + rootTable: PgTable, + whereExpr: SQL, + overrideParams?: unknown[], +): WhereClauseResult { + const compiled = queryBuilder.select({ __electric_where__: sql`1` }).from(rootTable).where(whereExpr).toSQL() + const result = { + whereClause: extractWhereClause(compiled.sql), + params: overrideParams ?? compiled.params, + } + + return dedupeParams(result.whereClause, result.params) +} + /** * Build IN clause with sorted IDs using unqualified column name. * Uses column.name for Electric SQL compatibility (Electric requires unqualified column names). @@ -95,11 +164,7 @@ export function buildInClause(column: PgColumn, values: readon return { whereClause: "false", params: [] } } const sorted = [...values].sort() - const placeholders = sorted.map((_, i) => `$${i + 1}`).join(", ") - return { - whereClause: `"${column.name}" IN (${placeholders})`, - params: sorted, - } + return sqlToWhereClause(getRootTable(column), sql`${col(column)} IN ${buildParamList(sorted)}`) } /** @@ -119,11 +184,10 @@ export function buildInClauseWithDeletedAt( return { whereClause: "false", params: [] } } const sorted = [...values].sort() - const placeholders = sorted.map((_, i) => `$${i + 1}`).join(", ") - return { - whereClause: `"${column.name}" IN (${placeholders}) AND "${deletedAtColumn.name}" IS NULL`, - params: sorted, - } + return sqlToWhereClause( + getRootTable(column), + sql`${col(column)} IN ${buildParamList(sorted)} AND ${isNullCol(deletedAtColumn)}`, + ) } /** @@ -135,9 +199,14 @@ export function buildInClauseWithDeletedAt( * @returns WhereClauseResult with parameterized WHERE clause */ export function buildEqClause(column: PgColumn, value: T, paramIndex = 1): WhereClauseResult { + const result = sqlToWhereClause(getRootTable(column), eqCol(column, value)) + if (paramIndex === 1) { + return result + } + return { - whereClause: `"${column.name}" = $${paramIndex}`, - params: [value], + whereClause: result.whereClause.replace(/\$1\b/g, `$${paramIndex}`), + params: result.params, } } @@ -148,10 +217,7 @@ export function buildEqClause(column: PgColumn, value: T, paramIndex = 1): Wh * @returns WhereClauseResult with no parameters */ export function buildDeletedAtNullClause(deletedAtColumn: PgColumn): WhereClauseResult { - return { - whereClause: `"${deletedAtColumn.name}" IS NULL`, - params: [], - } + return sqlToWhereClause(getRootTable(deletedAtColumn), isNullCol(deletedAtColumn)) } /** @@ -177,12 +243,16 @@ export function buildNoFilterClause(): WhereClauseResult { * @returns WhereClauseResult with parameterized WHERE clause and subquery */ export function buildChannelVisibilityClause(userId: UserId, deletedAtColumn: PgColumn): WhereClauseResult { - const whereClause = `"${deletedAtColumn.name}" IS NULL AND "id" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1)` + const channelAccessSubquery = buildSubquery( + schema.channelAccessTable.channelId, + schema.channelAccessTable, + eqCol(schema.channelAccessTable.userId, userId), + ) - return { - whereClause, - params: [userId], - } + return sqlToWhereClause( + getRootTable(deletedAtColumn), + sql`${isNullCol(deletedAtColumn)} AND ${inSubquery(schema.channelsTable.id, channelAccessSubquery)}`, + ) } /** @@ -201,9 +271,18 @@ export function buildOrgMembershipClause( orgIdColumn: PgColumn, deletedAtColumn?: PgColumn, ): WhereClauseResult { - const deletedAtClause = deletedAtColumn ? `"${deletedAtColumn.name}" IS NULL AND ` : "" - const whereClause = `${deletedAtClause}"${orgIdColumn.name}" IN (SELECT "organizationId" FROM organization_members WHERE "userId" = $1 AND "deletedAt" IS NULL)` - return { whereClause, params: [userId] } + const orgMembershipSubquery = buildSubquery( + schema.organizationMembersTable.organizationId, + schema.organizationMembersTable, + sql`${eqCol(schema.organizationMembersTable.userId, userId)} AND ${isNullCol(schema.organizationMembersTable.deletedAt)}`, + ) + + const baseCondition = inSubquery(orgIdColumn, orgMembershipSubquery) + const whereExpr = deletedAtColumn + ? sql`${isNullCol(deletedAtColumn)} AND ${baseCondition}` + : baseCondition + + return sqlToWhereClause(getRootTable(orgIdColumn), whereExpr) } /** @@ -222,9 +301,21 @@ export function buildUserOrgMembershipClause( userIdColumn: PgColumn, deletedAtColumn: PgColumn, ): WhereClauseResult { - // Filter users to those in same orgs as the current user - const whereClause = `"${deletedAtColumn.name}" IS NULL AND "${userIdColumn.name}" IN (SELECT "userId" FROM organization_members WHERE "organizationId" IN (SELECT "organizationId" FROM organization_members WHERE "userId" = $1 AND "deletedAt" IS NULL) AND "deletedAt" IS NULL)` - return { whereClause, params: [userId] } + const currentUserOrgIds = buildSubquery( + schema.organizationMembersTable.organizationId, + schema.organizationMembersTable, + sql`${eqCol(schema.organizationMembersTable.userId, userId)} AND ${isNullCol(schema.organizationMembersTable.deletedAt)}`, + ) + const sharedOrgUsers = buildSubquery( + schema.organizationMembersTable.userId, + schema.organizationMembersTable, + sql`${col(schema.organizationMembersTable.organizationId)} IN ${currentUserOrgIds} AND ${isNullCol(schema.organizationMembersTable.deletedAt)}`, + ) + + return sqlToWhereClause( + getRootTable(userIdColumn), + sql`${isNullCol(deletedAtColumn)} AND ${inSubquery(userIdColumn, sharedOrgUsers)}`, + ) } /** @@ -238,8 +329,13 @@ export function buildUserOrgMembershipClause( * @returns WhereClauseResult with parameterized WHERE clause and subquery */ export function buildUserMembershipClause(userId: UserId, memberIdColumn: PgColumn): WhereClauseResult { - const whereClause = `"${memberIdColumn.name}" IN (SELECT "id" FROM organization_members WHERE "userId" = $1 AND "deletedAt" IS NULL)` - return { whereClause, params: [userId] } + const membershipSubquery = buildSubquery( + schema.organizationMembersTable.id, + schema.organizationMembersTable, + sql`${eqCol(schema.organizationMembersTable.userId, userId)} AND ${isNullCol(schema.organizationMembersTable.deletedAt)}`, + ) + + return sqlToWhereClause(getRootTable(memberIdColumn), inSubquery(memberIdColumn, membershipSubquery)) } /** @@ -258,9 +354,17 @@ export function buildChannelAccessClause( channelIdColumn: PgColumn, deletedAtColumn?: PgColumn, ): WhereClauseResult { - const deletedAtClause = deletedAtColumn ? `"${deletedAtColumn.name}" IS NULL AND ` : "" - const whereClause = `${deletedAtClause}"${channelIdColumn.name}" IN (SELECT "channelId" FROM channel_access WHERE "userId" = $1)` - return { whereClause, params: [userId] } + const channelAccessSubquery = buildSubquery( + schema.channelAccessTable.channelId, + schema.channelAccessTable, + eqCol(schema.channelAccessTable.userId, userId), + ) + const baseCondition = inSubquery(channelIdColumn, channelAccessSubquery) + const whereExpr = deletedAtColumn + ? sql`${isNullCol(deletedAtColumn)} AND ${baseCondition}` + : baseCondition + + return sqlToWhereClause(getRootTable(channelIdColumn), whereExpr) } /** @@ -277,9 +381,16 @@ export function buildIntegrationConnectionClause( userId: UserId, deletedAtColumn: PgColumn, ): WhereClauseResult { - // Org-level connections (userId IS NULL) in user's orgs OR user's own connections - const whereClause = `"${deletedAtColumn.name}" IS NULL AND (("userId" IS NULL AND "organizationId" IN (SELECT "organizationId" FROM organization_members WHERE "userId" = $1 AND "deletedAt" IS NULL)) OR "userId" = $1)` - return { whereClause, params: [userId] } + const orgMembershipSubquery = buildSubquery( + schema.organizationMembersTable.organizationId, + schema.organizationMembersTable, + sql`${eqCol(schema.organizationMembersTable.userId, userId)} AND ${isNullCol(schema.organizationMembersTable.deletedAt)}`, + ) + + return sqlToWhereClause( + getRootTable(deletedAtColumn), + sql`${isNullCol(deletedAtColumn)} AND ((${sql.identifier("userId")} IS NULL AND ${sql.identifier("organizationId")} IN ${orgMembershipSubquery}) OR ${sql.identifier("userId")} = ${userId})`, + ) } /** From 2474db7f93f51393b4a3589b448518965af0fb4b Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 17 Mar 2026 13:45:12 +0100 Subject: [PATCH 32/34] fix --- apps/electric-proxy/src/tables/user-tables.test.ts | 12 +++++++++--- .../src/tables/where-clause-builder.ts | 11 +++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/electric-proxy/src/tables/user-tables.test.ts b/apps/electric-proxy/src/tables/user-tables.test.ts index 439ad5fa7..816151145 100644 --- a/apps/electric-proxy/src/tables/user-tables.test.ts +++ b/apps/electric-proxy/src/tables/user-tables.test.ts @@ -17,7 +17,9 @@ describe("user table where clauses", () => { expect(result.params).toEqual([testUser.internalUserId]) expect(result.whereClause).toContain(`"deletedAt" IS NULL`) - expect(result.whereClause).toMatch(/IN \(SELECT "conversationId" FROM "?connect_conversation_channels"?/) + expect(result.whereClause).toMatch( + /IN \(SELECT "conversationId" FROM "?connect_conversation_channels"?/, + ) expect(result.whereClause).toMatch(/"channelId" IN \(SELECT "channelId" FROM "?channel_access"?/) }) @@ -51,7 +53,9 @@ describe("user table where clauses", () => { ) expect(result.whereClause).toContain(`"conversationId" IS NULL`) expect(result.whereClause).toContain(`"conversationId" IS NOT NULL`) - expect(result.whereClause).toMatch(/IN \(SELECT "conversationId" FROM "?connect_conversation_channels"?/) + expect(result.whereClause).toMatch( + /IN \(SELECT "conversationId" FROM "?connect_conversation_channels"?/, + ) }) it("filters message reactions by channel access and conversation access", async () => { @@ -63,6 +67,8 @@ describe("user table where clauses", () => { ) expect(result.whereClause).toContain(`"conversationId" IS NULL`) expect(result.whereClause).toContain(`"conversationId" IS NOT NULL`) - expect(result.whereClause).toMatch(/IN \(SELECT "conversationId" FROM "?connect_conversation_channels"?/) + expect(result.whereClause).toMatch( + /IN \(SELECT "conversationId" FROM "?connect_conversation_channels"?/, + ) }) }) diff --git a/apps/electric-proxy/src/tables/where-clause-builder.ts b/apps/electric-proxy/src/tables/where-clause-builder.ts index 84e38eddd..2a7486cf7 100644 --- a/apps/electric-proxy/src/tables/where-clause-builder.ts +++ b/apps/electric-proxy/src/tables/where-clause-builder.ts @@ -98,7 +98,10 @@ export const inSubquery = (column: PgColumn, subquerySql: SQL): SQL => sql`${col const comma = sql.raw(", ") const buildParamList = (values: readonly unknown[]): SQL => - sql`(${sql.join(values.map((value) => sql`${value}`), comma)})` + sql`(${sql.join( + values.map((value) => sql`${value}`), + comma, + )})` const buildSubquery = (selectedColumn: PgColumn, table: PgTable, whereExpr: SQL): SQL => sql`(SELECT ${col(selectedColumn)} FROM ${table} WHERE ${whereExpr})` @@ -142,7 +145,11 @@ export function sqlToWhereClause( whereExpr: SQL, overrideParams?: unknown[], ): WhereClauseResult { - const compiled = queryBuilder.select({ __electric_where__: sql`1` }).from(rootTable).where(whereExpr).toSQL() + const compiled = queryBuilder + .select({ __electric_where__: sql`1` }) + .from(rootTable) + .where(whereExpr) + .toSQL() const result = { whereClause: extractWhereClause(compiled.sql), params: overrideParams ?? compiled.params, From 502fc70f5cb95e1f5868f2933ce82461b6953f1b Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 17 Mar 2026 17:42:06 +0100 Subject: [PATCH 33/34] feat simlify model stuff --- apps/backend/src/routes/auth.http.test.ts | 4 +- .../src/services/bot-gateway-service.test.ts | 2 +- .../src/services/bot-gateway-service.ts | 6 +- apps/web/src/atoms/bot-atoms.ts | 4 +- apps/web/src/atoms/channel-webhook-atoms.ts | 2 +- apps/web/src/atoms/chat-query-atoms.ts | 6 +- .../src/atoms/github-subscription-atoms.ts | 2 +- apps/web/src/atoms/rss-subscription-atoms.ts | 2 +- .../chat-sync/add-channel-link-modal.tsx | 2 +- .../src/components/chat/author-identity.ts | 4 +- .../channel-files-documents-list.tsx | 4 +- .../channel-files-media-gallery-view.tsx | 4 +- .../channel-files-media-grid.tsx | 4 +- apps/web/src/components/chat/chat-header.tsx | 2 +- .../components/chat/image-viewer-modal.tsx | 4 +- .../components/chat/message-attachments.tsx | 4 +- .../src/components/chat/message-embeds.tsx | 4 +- .../components/chat/message-reply-section.tsx | 4 +- .../command-palette/search-result-item.tsx | 6 +- apps/web/src/components/gif-embed.tsx | 2 +- .../add-github-subscription-modal.tsx | 2 +- .../add-rss-subscription-modal.tsx | 2 +- .../configure-openstatus-modal.tsx | 2 +- .../integrations/configure-railway-modal.tsx | 2 +- .../github-subscriptions-section.tsx | 2 +- .../openstatus-integration-content.tsx | 2 +- .../railway-integration-content.tsx | 2 +- .../rss-subscriptions-section.tsx | 2 +- .../src/components/modals/create-dm-modal.tsx | 2 +- apps/web/src/components/tweet-embed.tsx | 2 +- apps/web/src/db/collections.ts | 50 ++--- apps/web/src/db/hooks.ts | 12 +- apps/web/src/hooks/use-notifications.ts | 16 +- apps/web/src/hooks/use-search-query.ts | 6 +- apps/web/src/hooks/use-typing-indicators.ts | 4 +- apps/web/src/lib/channels.ts | 4 +- apps/web/src/lib/native-notifications.ts | 12 +- apps/web/src/lib/notifications/selectors.ts | 2 +- apps/web/src/lib/notifications/types.ts | 8 +- apps/web/src/providers/chat-provider.tsx | 4 +- .../routes/_app/$orgSlug/settings/debug.tsx | 2 +- .../settings/integrations/$integrationId.tsx | 2 +- apps/web/src/utils/attachment-url.ts | 2 +- libs/bot-sdk/src/hazel-bot-sdk.ts | 6 +- .../src/collection.ts | 2 +- .../src/repositories/attachment-repo.ts | 4 +- .../src/repositories/bot-command-repo.ts | 4 +- .../src/repositories/bot-installation-repo.ts | 6 +- .../backend-core/src/repositories/bot-repo.ts | 4 +- .../src/repositories/channel-member-repo.ts | 6 +- .../src/repositories/channel-repo.ts | 4 +- .../src/repositories/channel-section-repo.ts | 6 +- .../src/repositories/channel-webhook-repo.ts | 6 +- .../chat-sync-channel-link-repo.ts | 8 +- .../repositories/chat-sync-connection-repo.ts | 6 +- .../chat-sync-event-receipt-repo.ts | 6 +- .../chat-sync-message-link-repo.ts | 8 +- .../connect-conversation-channel-repo.ts | 6 +- .../repositories/connect-conversation-repo.ts | 6 +- .../src/repositories/connect-invite-repo.ts | 6 +- .../repositories/connect-participant-repo.ts | 6 +- .../src/repositories/custom-emoji-repo.ts | 4 +- .../repositories/github-subscription-repo.ts | 6 +- .../integration-connection-repo.ts | 6 +- .../repositories/integration-token-repo.ts | 6 +- .../src/repositories/invitation-repo.ts | 4 +- .../src/repositories/message-reaction-repo.ts | 6 +- .../src/repositories/message-repo.ts | 4 +- .../src/repositories/notification-repo.ts | 6 +- .../repositories/organization-member-repo.ts | 6 +- .../src/repositories/organization-repo.ts | 6 +- .../src/repositories/pinned-message-repo.ts | 6 +- .../src/repositories/rss-subscription-repo.ts | 6 +- .../src/repositories/typing-indicator-repo.ts | 6 +- .../repositories/user-presence-status-repo.ts | 6 +- .../src/repositories/user-repo.ts | 4 +- packages/db/README.md | 17 +- packages/db/src/services/index.ts | 3 +- packages/db/src/services/model-repository.ts | 115 ------------ packages/db/src/services/model.ts | 71 ------- packages/db/src/services/repository.ts | 175 ++++++++++++++++++ packages/domain/src/bot-gateway.ts | 16 +- packages/domain/src/http/api-v1/messages.ts | 6 +- packages/domain/src/http/chat-sync.ts | 8 +- .../domain/src/models/attachment-model.ts | 24 +-- .../domain/src/models/bot-command-model.ts | 32 ++-- .../src/models/bot-installation-model.ts | 8 +- packages/domain/src/models/bot-model.ts | 28 +-- .../domain/src/models/channel-member-model.ts | 20 +- packages/domain/src/models/channel-model.ts | 20 +- .../src/models/channel-section-model.ts | 12 +- .../src/models/channel-webhook-model.ts | 22 +-- .../models/chat-sync-channel-link-model.ts | 66 +++---- .../src/models/chat-sync-connection-model.ts | 34 ++-- .../models/chat-sync-event-receipt-model.ts | 26 +-- .../models/chat-sync-message-link-model.ts | 20 +- .../connect-conversation-channel-model.ts | 20 +- .../src/models/connect-conversation-model.ts | 18 +- .../domain/src/models/connect-invite-model.ts | 32 ++-- .../src/models/connect-participant-model.ts | 16 +- .../domain/src/models/custom-emoji-model.ts | 16 +- .../src/models/github-subscription-model.ts | 24 +-- .../models/integration-connection-model.ts | 38 ++-- .../src/models/integration-request-model.ts | 18 +- .../src/models/integration-token-model.ts | 28 +-- .../domain/src/models/invitation-model.ts | 22 +-- .../models/message-integration-link-model.ts | 20 +- packages/domain/src/models/message-model.ts | 39 ++-- .../src/models/message-reaction-model.ts | 17 +- .../domain/src/models/notification-model.ts | 18 +- .../src/models/organization-member-model.ts | 18 +- .../domain/src/models/organization-model.ts | 18 +- .../domain/src/models/pinned-message-model.ts | 6 +- .../src/models/rss-subscription-model.ts | 34 ++-- .../src/models/schema-migration.test.ts | 123 ++++++++++++ .../src/models/typing-indicator-model.ts | 10 +- packages/domain/src/models/user-model.ts | 48 ++--- .../src/models/user-presence-status-model.ts | 22 +-- packages/domain/src/models/utils.ts | 78 ++++++-- packages/domain/src/rpc/attachments.ts | 2 +- packages/domain/src/rpc/bots.ts | 10 +- packages/domain/src/rpc/channel-members.ts | 4 +- packages/domain/src/rpc/channel-sections.ts | 6 +- packages/domain/src/rpc/channel-webhooks.ts | 6 +- packages/domain/src/rpc/channels.ts | 6 +- packages/domain/src/rpc/chat-sync.ts | 8 +- packages/domain/src/rpc/connect-shares.ts | 8 +- packages/domain/src/rpc/custom-emojis.ts | 2 +- .../domain/src/rpc/github-subscriptions.ts | 4 +- .../domain/src/rpc/integration-requests.ts | 2 +- packages/domain/src/rpc/invitations.ts | 6 +- packages/domain/src/rpc/message-reactions.ts | 6 +- packages/domain/src/rpc/messages.ts | 4 +- packages/domain/src/rpc/notifications.ts | 6 +- .../domain/src/rpc/organization-members.ts | 6 +- packages/domain/src/rpc/organizations.ts | 6 +- packages/domain/src/rpc/pinned-messages.ts | 4 +- packages/domain/src/rpc/rss-subscriptions.ts | 4 +- packages/domain/src/rpc/typing-indicators.ts | 2 +- .../domain/src/rpc/user-presence-status.ts | 6 +- packages/domain/src/rpc/users.ts | 4 +- 141 files changed, 1044 insertions(+), 872 deletions(-) delete mode 100644 packages/db/src/services/model-repository.ts delete mode 100644 packages/db/src/services/model.ts create mode 100644 packages/db/src/services/repository.ts create mode 100644 packages/domain/src/models/schema-migration.test.ts diff --git a/apps/backend/src/routes/auth.http.test.ts b/apps/backend/src/routes/auth.http.test.ts index 2e88ae2ee..7d535249e 100644 --- a/apps/backend/src/routes/auth.http.test.ts +++ b/apps/backend/src/routes/auth.http.test.ts @@ -51,7 +51,7 @@ const makeJwt = (exp: number = Math.floor(Date.now() / 1000) + 3600) => { return `${encode({ alg: "none", typ: "JWT" })}.${encode({ exp, sid: "session_test_123" })}.` } -const makeUserRecord = (overrides: Partial> = {}) => +const makeUserRecord = (overrides: Partial> = {}) => ({ id: "usr_default123" as UserId, externalId: "user_default", @@ -67,7 +67,7 @@ const makeUserRecord = (overrides: Partial updatedAt: NOW, deletedAt: null, ...overrides, - }) satisfies Schema.Schema.Type + }) satisfies Schema.Schema.Type // ===== Mock WorkOS Service ===== diff --git a/apps/backend/src/services/bot-gateway-service.test.ts b/apps/backend/src/services/bot-gateway-service.test.ts index ab5632552..fb87e66e2 100644 --- a/apps/backend/src/services/bot-gateway-service.test.ts +++ b/apps/backend/src/services/bot-gateway-service.test.ts @@ -120,7 +120,7 @@ describe("BotGatewayService", () => { createdAt: new Date("2026-03-05T12:00:00.000Z"), updatedAt: null, deletedAt: null, - } satisfies Schema.Schema.Type + } satisfies Schema.Schema.Type globalThis.fetch = (async (input, init) => { requests.push({ diff --git a/apps/backend/src/services/bot-gateway-service.ts b/apps/backend/src/services/bot-gateway-service.ts index 3ac908bb9..d43033113 100644 --- a/apps/backend/src/services/bot-gateway-service.ts +++ b/apps/backend/src/services/bot-gateway-service.ts @@ -183,7 +183,7 @@ export class BotGatewayService extends ServiceMap.Service()(" const publishMessageEvent = Effect.fn("BotGatewayService.publishMessageEvent")(function* ( eventType: "message.create" | "message.update" | "message.delete", - message: Schema.Schema.Type, + message: Schema.Schema.Type, ) { const organizationId = yield* resolveOrganizationIdForChannel(message.channelId) if (!organizationId) { @@ -212,7 +212,7 @@ export class BotGatewayService extends ServiceMap.Service()(" const publishChannelEvent = Effect.fn("BotGatewayService.publishChannelEvent")(function* ( eventType: "channel.create" | "channel.update" | "channel.delete", - channel: Schema.Schema.Type, + channel: Schema.Schema.Type, ) { const eventTimestamp = channel.updatedAt ? toEpochMs(channel.updatedAt) @@ -236,7 +236,7 @@ export class BotGatewayService extends ServiceMap.Service()(" const publishChannelMemberEvent = Effect.fn("BotGatewayService.publishChannelMemberEvent")(function* ( eventType: "channel_member.add" | "channel_member.remove", - member: Schema.Schema.Type, + member: Schema.Schema.Type, ) { const organizationId = yield* resolveOrganizationIdForChannel(member.channelId) if (!organizationId) { diff --git a/apps/web/src/atoms/bot-atoms.ts b/apps/web/src/atoms/bot-atoms.ts index 8970a2e4e..d9023bf1c 100644 --- a/apps/web/src/atoms/bot-atoms.ts +++ b/apps/web/src/atoms/bot-atoms.ts @@ -7,12 +7,12 @@ import { HazelRpcClient } from "~/lib/services/common/rpc-atom-client" * Type for bot data returned from RPC. * Inferred from the domain model's JSON schema to stay in sync automatically. */ -export type BotData = Schema.Schema.Type +export type BotData = Schema.Schema.Type /** * Type for bot command data returned from RPC. */ -export type BotCommandData = Schema.Schema.Type +export type BotCommandData = Schema.Schema.Type /** * Type for public bot data with install status. diff --git a/apps/web/src/atoms/channel-webhook-atoms.ts b/apps/web/src/atoms/channel-webhook-atoms.ts index 290a7e596..e335888b1 100644 --- a/apps/web/src/atoms/channel-webhook-atoms.ts +++ b/apps/web/src/atoms/channel-webhook-atoms.ts @@ -6,7 +6,7 @@ import { HazelRpcClient } from "~/lib/services/common/rpc-atom-client" * Type for webhook data returned from RPC (without sensitive tokenHash field). * Inferred from the domain model's JSON schema to stay in sync automatically. */ -export type WebhookData = Schema.Schema.Type +export type WebhookData = Schema.Schema.Type /** * Mutation atom for creating a channel webhook. diff --git a/apps/web/src/atoms/chat-query-atoms.ts b/apps/web/src/atoms/chat-query-atoms.ts index 97a220191..3915be2ea 100644 --- a/apps/web/src/atoms/chat-query-atoms.ts +++ b/apps/web/src/atoms/chat-query-atoms.ts @@ -5,9 +5,9 @@ import { eq } from "@tanstack/db" import { channelCollection } from "~/db/collections" import { makeQuery } from "../../../../libs/tanstack-db-atom/src" -export type MessageWithPinned = typeof Message.Model.Type & { - pinnedMessage: typeof PinnedMessage.Model.Type | null | undefined - author: typeof User.Model.Type | null | undefined +export type MessageWithPinned = Message.Type & { + pinnedMessage: PinnedMessage.Type | null | undefined + author: User.Type | null | undefined isSyncedFromDiscord?: boolean } diff --git a/apps/web/src/atoms/github-subscription-atoms.ts b/apps/web/src/atoms/github-subscription-atoms.ts index 417199e97..489c63b68 100644 --- a/apps/web/src/atoms/github-subscription-atoms.ts +++ b/apps/web/src/atoms/github-subscription-atoms.ts @@ -6,7 +6,7 @@ import { HazelRpcClient } from "~/lib/services/common/rpc-atom-client" * Type for GitHub subscription data returned from RPC. * Inferred from the domain model's JSON schema to stay in sync automatically. */ -export type GitHubSubscriptionData = Schema.Schema.Type +export type GitHubSubscriptionData = Schema.Schema.Type /** * Mutation atom for creating a GitHub subscription. diff --git a/apps/web/src/atoms/rss-subscription-atoms.ts b/apps/web/src/atoms/rss-subscription-atoms.ts index eb8dc39d4..c47877a5b 100644 --- a/apps/web/src/atoms/rss-subscription-atoms.ts +++ b/apps/web/src/atoms/rss-subscription-atoms.ts @@ -6,7 +6,7 @@ import { HazelRpcClient } from "~/lib/services/common/rpc-atom-client" * Type for RSS subscription data returned from RPC. * Inferred from the domain model's JSON schema to stay in sync automatically. */ -export type RssSubscriptionData = Schema.Schema.Type +export type RssSubscriptionData = Schema.Schema.Type /** * Mutation atom for creating an RSS subscription. diff --git a/apps/web/src/components/chat-sync/add-channel-link-modal.tsx b/apps/web/src/components/chat-sync/add-channel-link-modal.tsx index 1245bd011..398ea9caa 100644 --- a/apps/web/src/components/chat-sync/add-channel-link-modal.tsx +++ b/apps/web/src/components/chat-sync/add-channel-link-modal.tsx @@ -22,7 +22,7 @@ import { HazelApiClient } from "~/lib/services/common/atom-client" import { HazelRpcClient } from "~/lib/services/common/rpc-atom-client" import { exitToast } from "~/lib/toast-exit" -type ChannelData = typeof Channel.Model.Type +type ChannelData = Channel.Type type SyncDirection = "both" | "hazel_to_external" | "external_to_hazel" interface DiscordChannel { diff --git a/apps/web/src/components/chat/author-identity.ts b/apps/web/src/components/chat/author-identity.ts index c6e240c38..5717e6a6f 100644 --- a/apps/web/src/components/chat/author-identity.ts +++ b/apps/web/src/components/chat/author-identity.ts @@ -19,7 +19,7 @@ export interface ChatAuthorIdentity { } export function buildChatAuthorIdentity( - user: ChatAuthorUserLike | typeof User.Model.Type | null | undefined, + user: ChatAuthorUserLike | User.Type | null | undefined, botName?: string | null, ): ChatAuthorIdentity { if (!user) { @@ -45,7 +45,7 @@ export function buildChatAuthorIdentity( export function useChatAuthorIdentity( userId: UserId | undefined, - user: ChatAuthorUserLike | typeof User.Model.Type | null | undefined, + user: ChatAuthorUserLike | User.Type | null | undefined, ): ChatAuthorIdentity { const botName = useBotName(userId, user?.userType) return buildChatAuthorIdentity(user, botName) diff --git a/apps/web/src/components/chat/channel-files/channel-files-documents-list.tsx b/apps/web/src/components/chat/channel-files/channel-files-documents-list.tsx index 3e82d8b80..fda92742d 100644 --- a/apps/web/src/components/chat/channel-files/channel-files-documents-list.tsx +++ b/apps/web/src/components/chat/channel-files/channel-files-documents-list.tsx @@ -8,8 +8,8 @@ import { getAttachmentUrl } from "~/utils/attachment-url" import { formatFileSize, getFileTypeFromName } from "~/utils/file-utils" import { useChatAuthorIdentity } from "../author-identity" -type AttachmentWithUser = typeof Attachment.Model.Type & { - user: typeof User.Model.Type | null +type AttachmentWithUser = Attachment.Type & { + user: User.Type | null } interface ChannelFilesDocumentsListProps { diff --git a/apps/web/src/components/chat/channel-files/channel-files-media-gallery-view.tsx b/apps/web/src/components/chat/channel-files/channel-files-media-gallery-view.tsx index 1680e1577..c827b5045 100644 --- a/apps/web/src/components/chat/channel-files/channel-files-media-gallery-view.tsx +++ b/apps/web/src/components/chat/channel-files/channel-files-media-gallery-view.tsx @@ -10,8 +10,8 @@ import { getAttachmentUrl } from "~/utils/attachment-url" import { getFileCategory, getFileTypeFromName } from "~/utils/file-utils" import { ImageViewerModal, type ViewerImage } from "../image-viewer-modal" -type AttachmentWithUser = typeof Attachment.Model.Type & { - user: typeof User.Model.Type | null +type AttachmentWithUser = Attachment.Type & { + user: User.Type | null } interface MediaGalleryViewProps { diff --git a/apps/web/src/components/chat/channel-files/channel-files-media-grid.tsx b/apps/web/src/components/chat/channel-files/channel-files-media-grid.tsx index 1004fdf93..a873e0e4e 100644 --- a/apps/web/src/components/chat/channel-files/channel-files-media-grid.tsx +++ b/apps/web/src/components/chat/channel-files/channel-files-media-grid.tsx @@ -11,8 +11,8 @@ import { getAttachmentUrl } from "~/utils/attachment-url" import { getFileTypeFromName } from "~/utils/file-utils" import { ImageViewerModal, type ViewerImage } from "../image-viewer-modal" -type AttachmentWithUser = typeof Attachment.Model.Type & { - user: typeof User.Model.Type | null +type AttachmentWithUser = Attachment.Type & { + user: User.Type | null } interface ChannelFilesMediaGridProps { diff --git a/apps/web/src/components/chat/chat-header.tsx b/apps/web/src/components/chat/chat-header.tsx index 4dde52bc2..68a2032f1 100644 --- a/apps/web/src/components/chat/chat-header.tsx +++ b/apps/web/src/components/chat/chat-header.tsx @@ -24,7 +24,7 @@ import { PinnedMessagesModal } from "./pinned-messages-modal" interface OtherMemberAvatarProps { member: { userId: UserId - user: Pick + user: Pick } } diff --git a/apps/web/src/components/chat/image-viewer-modal.tsx b/apps/web/src/components/chat/image-viewer-modal.tsx index 6b6073713..e7943fccb 100644 --- a/apps/web/src/components/chat/image-viewer-modal.tsx +++ b/apps/web/src/components/chat/image-viewer-modal.tsx @@ -20,7 +20,7 @@ import { IconExternalLink } from "../icons/icon-link-external" export type ViewerImage = | { type: "attachment" - attachment: typeof Attachment.Model.Type + attachment: Attachment.Type } | { type: "url" @@ -33,7 +33,7 @@ interface ImageViewerModalProps { onOpenChange: (open: boolean) => void images: ViewerImage[] initialIndex: number - author?: typeof User.Model.Type + author?: User.Type createdAt: number } diff --git a/apps/web/src/components/chat/message-attachments.tsx b/apps/web/src/components/chat/message-attachments.tsx index 2101a7254..803d5c07d 100644 --- a/apps/web/src/components/chat/message-attachments.tsx +++ b/apps/web/src/components/chat/message-attachments.tsx @@ -17,7 +17,7 @@ interface MessageAttachmentsProps { } interface ImageAttachmentItemProps { - attachment: typeof Attachment.Model.Type + attachment: Attachment.Type imageCount: number index: number onClick: () => void @@ -52,7 +52,7 @@ function ImageAttachmentItem({ attachment, imageCount, index, onClick }: ImageAt } interface AttachmentItemProps { - attachment: typeof Attachment.Model.Type + attachment: Attachment.Type } function AttachmentItem({ attachment }: AttachmentItemProps) { diff --git a/apps/web/src/components/chat/message-embeds.tsx b/apps/web/src/components/chat/message-embeds.tsx index f5f399fba..ebe524fcd 100644 --- a/apps/web/src/components/chat/message-embeds.tsx +++ b/apps/web/src/components/chat/message-embeds.tsx @@ -4,10 +4,10 @@ import { Embed } from "~/components/embeds" import { MessageLive } from "./message-live-state" // Extract embed type from the Message model -type MessageEmbedType = NonNullable[number] +type MessageEmbedType = NonNullable[number] interface MessageEmbedsProps { - embeds: typeof Message.Model.Type.embeds + embeds: Message.Type["embeds"] messageId?: MessageId organizationId?: OrganizationId } diff --git a/apps/web/src/components/chat/message-reply-section.tsx b/apps/web/src/components/chat/message-reply-section.tsx index fd3d6799c..a79fb043d 100644 --- a/apps/web/src/components/chat/message-reply-section.tsx +++ b/apps/web/src/components/chat/message-reply-section.tsx @@ -11,8 +11,8 @@ interface MessageReplySectionProps { onClick?: () => void } -type MessageWithAuthor = typeof Message.Model.Type & { - author: typeof User.Model.Type +type MessageWithAuthor = Message.Type & { + author: User.Type } export function MessageReplySection({ replyToMessageId, onClick }: MessageReplySectionProps) { diff --git a/apps/web/src/components/command-palette/search-result-item.tsx b/apps/web/src/components/command-palette/search-result-item.tsx index c32574a86..e9f928da9 100644 --- a/apps/web/src/components/command-palette/search-result-item.tsx +++ b/apps/web/src/components/command-palette/search-result-item.tsx @@ -8,9 +8,9 @@ import { cn, toDate } from "~/lib/utils" import { MarkdownText } from "./markdown-text" interface SearchResultItemProps { - message: typeof Message.Model.Type - author: typeof User.Model.Type | null - channel: typeof Channel.Model.Type | null + message: Message.Type + author: User.Type | null + channel: Channel.Type | null attachmentCount: number searchQuery?: string isSelected?: boolean diff --git a/apps/web/src/components/gif-embed.tsx b/apps/web/src/components/gif-embed.tsx index bd6a9ec80..0c632af23 100644 --- a/apps/web/src/components/gif-embed.tsx +++ b/apps/web/src/components/gif-embed.tsx @@ -5,7 +5,7 @@ import { extractGiphyMediaUrl, isKlipyUrl } from "~/components/link-preview" interface GifEmbedProps { url: string - author?: typeof User.Model.Type + author?: User.Type createdAt?: number } diff --git a/apps/web/src/components/integrations/add-github-subscription-modal.tsx b/apps/web/src/components/integrations/add-github-subscription-modal.tsx index 21ae421da..cd295838f 100644 --- a/apps/web/src/components/integrations/add-github-subscription-modal.tsx +++ b/apps/web/src/components/integrations/add-github-subscription-modal.tsx @@ -23,7 +23,7 @@ import { HazelApiClient } from "~/lib/services/common/atom-client" import { exitToast } from "~/lib/toast-exit" type GitHubEventType = typeof GitHubSubscription.GitHubEventType.Type -type ChannelData = typeof Channel.Model.Type +type ChannelData = Channel.Type const EVENT_OPTIONS: { id: GitHubEventType; label: string; description: string }[] = [ { id: "push", label: "Push", description: "Commits pushed to branches" }, diff --git a/apps/web/src/components/integrations/add-rss-subscription-modal.tsx b/apps/web/src/components/integrations/add-rss-subscription-modal.tsx index a7248b559..8ec2e2678 100644 --- a/apps/web/src/components/integrations/add-rss-subscription-modal.tsx +++ b/apps/web/src/components/integrations/add-rss-subscription-modal.tsx @@ -19,7 +19,7 @@ import { import { channelCollection } from "~/db/collections" import { exitToast } from "~/lib/toast-exit" -type ChannelData = typeof Channel.Model.Type +type ChannelData = Channel.Type const POLLING_INTERVAL_OPTIONS = [ { value: 5, label: "Every 5 minutes" }, diff --git a/apps/web/src/components/integrations/configure-openstatus-modal.tsx b/apps/web/src/components/integrations/configure-openstatus-modal.tsx index 09f70ea0c..39a7520eb 100644 --- a/apps/web/src/components/integrations/configure-openstatus-modal.tsx +++ b/apps/web/src/components/integrations/configure-openstatus-modal.tsx @@ -10,7 +10,7 @@ import { Description } from "~/components/ui/field" import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ModalTitle } from "~/components/ui/modal" import { Select, SelectContent, SelectItem, SelectTrigger } from "~/components/ui/select" -type ChannelData = typeof Channel.Model.Type +type ChannelData = Channel.Type interface ConfigureOpenStatusModalProps { isOpen: boolean diff --git a/apps/web/src/components/integrations/configure-railway-modal.tsx b/apps/web/src/components/integrations/configure-railway-modal.tsx index 42db78c31..1c6da69f4 100644 --- a/apps/web/src/components/integrations/configure-railway-modal.tsx +++ b/apps/web/src/components/integrations/configure-railway-modal.tsx @@ -10,7 +10,7 @@ import { Description } from "~/components/ui/field" import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ModalTitle } from "~/components/ui/modal" import { Select, SelectContent, SelectItem, SelectTrigger } from "~/components/ui/select" -type ChannelData = typeof Channel.Model.Type +type ChannelData = Channel.Type interface ConfigureRailwayModalProps { isOpen: boolean diff --git a/apps/web/src/components/integrations/github-subscriptions-section.tsx b/apps/web/src/components/integrations/github-subscriptions-section.tsx index 3626ff95a..4a6e03dca 100644 --- a/apps/web/src/components/integrations/github-subscriptions-section.tsx +++ b/apps/web/src/components/integrations/github-subscriptions-section.tsx @@ -26,7 +26,7 @@ import { exitToast } from "~/lib/toast-exit" import { AddGitHubSubscriptionModal } from "./add-github-subscription-modal" import { EditGitHubSubscriptionModal } from "./edit-github-subscription-modal" -type ChannelData = typeof Channel.Model.Type +type ChannelData = Channel.Type interface GitHubSubscriptionsSectionProps { organizationId: OrganizationId diff --git a/apps/web/src/components/integrations/openstatus-integration-content.tsx b/apps/web/src/components/integrations/openstatus-integration-content.tsx index 38700589d..23aab2975 100644 --- a/apps/web/src/components/integrations/openstatus-integration-content.tsx +++ b/apps/web/src/components/integrations/openstatus-integration-content.tsx @@ -18,7 +18,7 @@ import { channelCollection } from "~/db/collections" import { exitToast } from "~/lib/toast-exit" import { ConfigureOpenStatusModal } from "./configure-openstatus-modal" -type ChannelData = typeof Channel.Model.Type +type ChannelData = Channel.Type interface OpenStatusIntegrationContentProps { organizationId: OrganizationId diff --git a/apps/web/src/components/integrations/railway-integration-content.tsx b/apps/web/src/components/integrations/railway-integration-content.tsx index a4a78f3be..b44e92afa 100644 --- a/apps/web/src/components/integrations/railway-integration-content.tsx +++ b/apps/web/src/components/integrations/railway-integration-content.tsx @@ -18,7 +18,7 @@ import { channelCollection } from "~/db/collections" import { exitToast } from "~/lib/toast-exit" import { ConfigureRailwayModal } from "./configure-railway-modal" -type ChannelData = typeof Channel.Model.Type +type ChannelData = Channel.Type interface RailwayIntegrationContentProps { organizationId: OrganizationId diff --git a/apps/web/src/components/integrations/rss-subscriptions-section.tsx b/apps/web/src/components/integrations/rss-subscriptions-section.tsx index 0ae48da0e..418c825ed 100644 --- a/apps/web/src/components/integrations/rss-subscriptions-section.tsx +++ b/apps/web/src/components/integrations/rss-subscriptions-section.tsx @@ -23,7 +23,7 @@ import { channelCollection } from "~/db/collections" import { exitToast } from "~/lib/toast-exit" import { AddRssSubscriptionModal } from "./add-rss-subscription-modal" -type ChannelData = typeof Channel.Model.Type +type ChannelData = Channel.Type interface RssSubscriptionsSectionProps { organizationId: OrganizationId diff --git a/apps/web/src/components/modals/create-dm-modal.tsx b/apps/web/src/components/modals/create-dm-modal.tsx index 8c5852ec9..fec284bd7 100644 --- a/apps/web/src/components/modals/create-dm-modal.tsx +++ b/apps/web/src/components/modals/create-dm-modal.tsx @@ -165,7 +165,7 @@ export function CreateDmModal({ isOpen, onOpenChange }: CreateDmModalProps) { // Stable callback for toggling user selection - only updates form state const toggleUserSelection = useCallback( - (targetUser: typeof User.Model.Type) => { + (targetUser: User.Type) => { const currentIds = form.state.values.userIds const isSelected = currentIds.includes(targetUser.id) const newIds = isSelected diff --git a/apps/web/src/components/tweet-embed.tsx b/apps/web/src/components/tweet-embed.tsx index 567564bbb..d185059cb 100644 --- a/apps/web/src/components/tweet-embed.tsx +++ b/apps/web/src/components/tweet-embed.tsx @@ -232,7 +232,7 @@ function TweetMetrics({ tweet }: { tweet: EnrichedTweet }) { interface TweetEmbedProps { id: string - author?: typeof User.Model.Type + author?: User.Type messageCreatedAt?: number } diff --git a/apps/web/src/db/collections.ts b/apps/web/src/db/collections.ts index 5828f3f74..f93223b8c 100644 --- a/apps/web/src/db/collections.ts +++ b/apps/web/src/db/collections.ts @@ -48,7 +48,7 @@ export const organizationCollection = createEffectCollection({ fetchClient: electricFetchClient, }, - schema: Organization.Model.json, + schema: Organization.Schema, getKey: (item) => item.id, }) @@ -67,7 +67,7 @@ export const invitationCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: Invitation.Model.json, + schema: Invitation.Schema, getKey: (item) => item.id, }) @@ -87,7 +87,7 @@ export const messageCollection = createEffectCollection({ } as any, fetchClient: electricFetchClient, }, - schema: Message.Model.json, + schema: Message.Schema, getKey: (item) => item.id, }) @@ -106,7 +106,7 @@ export const messageReactionCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: MessageReaction.Model.json, + schema: MessageReaction.Schema, getKey: (item) => item.id, }) @@ -125,7 +125,7 @@ export const pinnedMessageCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: PinnedMessage.Model.json, + schema: PinnedMessage.Schema, getKey: (item) => item.id, }) @@ -145,7 +145,7 @@ export const notificationCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: Notification.Model.json, + schema: Notification.Schema, getKey: (item) => item.id, }) @@ -165,7 +165,7 @@ export const userCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: User.Model.json, + schema: User.Schema, getKey: (item) => item.id, }) @@ -184,7 +184,7 @@ export const organizationMemberCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: OrganizationMember.Model.json, + schema: OrganizationMember.Schema, getKey: (item) => item.id, }) @@ -203,7 +203,7 @@ export const channelCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: Channel.Model.json, + schema: Channel.Schema, getKey: (item) => item.id, }) @@ -221,7 +221,7 @@ export const connectConversationCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: ConnectConversation.Model.json, + schema: ConnectConversation.Schema, getKey: (item) => item.id, }) @@ -239,7 +239,7 @@ export const connectConversationChannelCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: ConnectConversationChannel.Model.json, + schema: ConnectConversationChannel.Schema, getKey: (item) => item.id, }) @@ -257,7 +257,7 @@ export const connectParticipantCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: ConnectParticipant.Model.json, + schema: ConnectParticipant.Schema, getKey: (item) => item.id, }) @@ -277,7 +277,7 @@ export const channelMemberCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: ChannelMember.Model.json, + schema: ChannelMember.Schema, getKey: (item) => item.id, }) @@ -295,7 +295,7 @@ export const channelSectionCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: ChannelSection.Model.json, + schema: ChannelSection.Schema, getKey: (item) => item.id, }) @@ -314,7 +314,7 @@ export const attachmentCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: Attachment.Model.json, + schema: Attachment.Schema, getKey: (item) => item.id, }) @@ -331,7 +331,7 @@ export const typingIndicatorCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: TypingIndicator.Model.json, + schema: TypingIndicator.Schema, getKey: (item) => item.id, }) @@ -350,7 +350,7 @@ export const userPresenceStatusCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: UserPresenceStatus.Model.json, + schema: UserPresenceStatus.Schema, getKey: (item) => item.id, }) @@ -370,7 +370,7 @@ export const integrationConnectionCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: IntegrationConnection.Model.json, + schema: IntegrationConnection.Schema, getKey: (item) => item.id, }) @@ -388,7 +388,7 @@ export const chatSyncConnectionCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: ChatSyncConnection.Model.json, + schema: ChatSyncConnection.Schema, getKey: (item) => item.id, }) @@ -406,7 +406,7 @@ export const chatSyncChannelLinkCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: ChatSyncChannelLink.Model.json, + schema: ChatSyncChannelLink.Schema, getKey: (item) => item.id, }) @@ -424,7 +424,7 @@ export const chatSyncMessageLinkCollection = createEffectCollection({ }, fetchClient: electricFetchClient, }, - schema: ChatSyncMessageLink.Model.json, + schema: ChatSyncMessageLink.Schema, getKey: (item) => item.id, }) @@ -442,7 +442,7 @@ export const botCollection = createEffectCollection({ } as any, fetchClient: electricFetchClient, }, - schema: Bot.Model.json, + schema: Bot.Schema, getKey: (item) => item.id, }) @@ -460,7 +460,7 @@ export const botCommandCollection = createEffectCollection({ } as any, fetchClient: electricFetchClient, }, - schema: BotCommand.Model.json, + schema: BotCommand.Schema, getKey: (item) => item.id, }) @@ -478,7 +478,7 @@ export const botInstallationCollection = createEffectCollection({ } as any, fetchClient: electricFetchClient, }, - schema: BotInstallation.Model.json, + schema: BotInstallation.Schema, getKey: (item) => item.id, }) @@ -496,6 +496,6 @@ export const customEmojiCollection = createEffectCollection({ } as any, fetchClient: electricFetchClient, }, - schema: CustomEmoji.Model.json, + schema: CustomEmoji.Schema, getKey: (item) => item.id, }) diff --git a/apps/web/src/db/hooks.ts b/apps/web/src/db/hooks.ts index 30adaab4e..63b9d860c 100644 --- a/apps/web/src/db/hooks.ts +++ b/apps/web/src/db/hooks.ts @@ -35,9 +35,9 @@ export const useMessage = (messageId: MessageId) => { } } -type ChannelWithMembers = typeof Channel.Model.Type & { - members: (typeof ChannelMember.Model.Type & { - user: typeof User.Model.Type +type ChannelWithMembers = Channel.Type & { + members: (ChannelMember.Type & { + user: User.Type })[] } @@ -199,7 +199,7 @@ export const useIntegrationConnections = (organizationId: OrganizationId | null) // Build a map of provider -> connection for easy lookup const connectionsByProvider = new Map< IntegrationConnection.IntegrationProvider, - typeof IntegrationConnection.Model.Type + IntegrationConnection.Type >() if (organizationId && data) { @@ -417,8 +417,8 @@ export const useBotName = (userId: UserId | undefined, userType: string | undefi /** * Type for bot data with its machine user included. */ -export type BotWithUser = typeof Bot.Model.Type & { - user: typeof User.Model.Type +export type BotWithUser = Bot.Type & { + user: User.Type } /** diff --git a/apps/web/src/hooks/use-notifications.ts b/apps/web/src/hooks/use-notifications.ts index 5df8796a2..8dfe011af 100644 --- a/apps/web/src/hooks/use-notifications.ts +++ b/apps/web/src/hooks/use-notifications.ts @@ -42,10 +42,10 @@ function useOrganizationMember() { } export interface NotificationWithDetails { - notification: typeof Notification.Model.Type - message?: typeof Message.Model.Type - channel?: typeof Channel.Model.Type - author?: typeof User.Model.Type + notification: Notification.Type + message?: Message.Type + channel?: Channel.Type + author?: User.Type } export function useNotifications() { @@ -76,10 +76,10 @@ export function useNotifications() { if (!notificationsData) return [] return notificationsData.map((row) => ({ - notification: row.notification as typeof Notification.Model.Type, - message: (row.message ?? undefined) as typeof Message.Model.Type | undefined, - channel: (row.channel ?? undefined) as typeof Channel.Model.Type | undefined, - author: (row.author ?? undefined) as typeof User.Model.Type | undefined, + notification: row.notification as Notification.Type, + message: (row.message ?? undefined) as Message.Type | undefined, + channel: (row.channel ?? undefined) as Channel.Type | undefined, + author: (row.author ?? undefined) as User.Type | undefined, })) }, [notificationsData]) diff --git a/apps/web/src/hooks/use-search-query.ts b/apps/web/src/hooks/use-search-query.ts index c473374e2..efc4c19c4 100644 --- a/apps/web/src/hooks/use-search-query.ts +++ b/apps/web/src/hooks/use-search-query.ts @@ -14,9 +14,9 @@ import { parseDateFilter, type SearchFilter } from "~/lib/search-filter-parser" import { getFileCategory } from "~/utils/file-utils" export interface SearchResult { - message: typeof Message.Model.Type - author: typeof User.Model.Type | null - channel: typeof Channel.Model.Type | null + message: Message.Type + author: User.Type | null + channel: Channel.Type | null attachmentCount: number } diff --git a/apps/web/src/hooks/use-typing-indicators.ts b/apps/web/src/hooks/use-typing-indicators.ts index 07678c8fe..b698194be 100644 --- a/apps/web/src/hooks/use-typing-indicators.ts +++ b/apps/web/src/hooks/use-typing-indicators.ts @@ -7,8 +7,8 @@ import { useAuth } from "~/lib/auth" import { pushTypingDiagnostics } from "~/lib/typing-diagnostics" type TypingUser = { - user: typeof User.Model.Type - member: typeof ChannelMember.Model.Type + user: User.Type + member: ChannelMember.Type } export type TypingUsers = TypingUser[] diff --git a/apps/web/src/lib/channels.ts b/apps/web/src/lib/channels.ts index 73f968454..8d9669ac0 100644 --- a/apps/web/src/lib/channels.ts +++ b/apps/web/src/lib/channels.ts @@ -31,7 +31,7 @@ export function findExistingDmChannel( currentUserId: UserId, targetUserIds: UserId[], organizationId: OrganizationId, -): typeof Channel.Model.Type | null { +): Channel.Type | null { const channels = dmChannelsCollection.toArray if (!channels || channels.length === 0 || targetUserIds.length === 0) { @@ -41,7 +41,7 @@ export function findExistingDmChannel( const allParticipants = [currentUserId, ...targetUserIds] // Group channels by channel ID to get all members per channel - const channelMembersMap = new Map() + const channelMembersMap = new Map() for (const item of channels) { // Skip channels from other organizations diff --git a/apps/web/src/lib/native-notifications.ts b/apps/web/src/lib/native-notifications.ts index c66c53159..65f5d6d61 100644 --- a/apps/web/src/lib/native-notifications.ts +++ b/apps/web/src/lib/native-notifications.ts @@ -192,7 +192,7 @@ export function getMessagePreview(content: string | null | undefined, maxLength /** * Format author display name */ -export function formatAuthorName(user: typeof User.Model.Type | undefined): string { +export function formatAuthorName(user: User.Type | undefined): string { if (!user) return "Someone" return `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() || "Someone" } @@ -203,8 +203,8 @@ export function formatAuthorName(user: typeof User.Model.Type | undefined): stri * - Channel/thread: Author + channel ("John Doe in #general") */ export function formatNotificationTitle( - author: typeof User.Model.Type | undefined, - channel: typeof Channel.Model.Type | undefined, + author: User.Type | undefined, + channel: Channel.Type | undefined, ): string { const authorName = formatAuthorName(author) @@ -221,9 +221,9 @@ export function formatNotificationTitle( * Build complete notification content from message, author, and channel data */ export function buildNotificationContent( - message: typeof Message.Model.Type | undefined, - author: typeof User.Model.Type | undefined, - channel: typeof Channel.Model.Type | undefined, + message: Message.Type | undefined, + author: User.Type | undefined, + channel: Channel.Type | undefined, ): NativeNotificationOptions { const title = formatNotificationTitle(author, channel) const body = getMessagePreview(message?.content) diff --git a/apps/web/src/lib/notifications/selectors.ts b/apps/web/src/lib/notifications/selectors.ts index 1a082199e..6acc8858a 100644 --- a/apps/web/src/lib/notifications/selectors.ts +++ b/apps/web/src/lib/notifications/selectors.ts @@ -5,7 +5,7 @@ import { useMemo } from "react" import { notificationCollection } from "~/db/collections" export type NotificationLike = Pick< - typeof Notification.Model.Type, + Notification.Type, "id" | "readAt" | "targetedResourceId" | "targetedResourceType" > diff --git a/apps/web/src/lib/notifications/types.ts b/apps/web/src/lib/notifications/types.ts index 243c039df..220930259 100644 --- a/apps/web/src/lib/notifications/types.ts +++ b/apps/web/src/lib/notifications/types.ts @@ -26,10 +26,10 @@ export interface NotificationSinkResult { export interface NotificationEvent { id: string - notification: typeof Notification.Model.Type - message?: typeof Message.Model.Type - author?: typeof User.Model.Type - channel?: typeof Channel.Model.Type + notification: Notification.Type + message?: Message.Type + author?: User.Type + channel?: Channel.Type receivedAt: number } diff --git a/apps/web/src/providers/chat-provider.tsx b/apps/web/src/providers/chat-provider.tsx index 90da4a825..d05bbd632 100644 --- a/apps/web/src/providers/chat-provider.tsx +++ b/apps/web/src/providers/chat-provider.tsx @@ -50,7 +50,7 @@ interface SendMessageProps { export interface ChatStableValue { channelId: ChannelId organizationId: OrganizationId - channel: typeof Channel.Model.Type | undefined + channel: Channel.Type | undefined // All actions (sendMessage is stabilized via refs) sendMessage: (props: SendMessageProps) => void editMessage: (messageId: MessageId, content: string) => Promise @@ -100,7 +100,7 @@ export interface ChatThreadValue { export interface ChatState { channelId: ChannelId organizationId: OrganizationId - channel: typeof Channel.Model.Type | undefined + channel: Channel.Type | undefined replyToMessageId: MessageId | null attachmentIds: AttachmentId[] isUploading: boolean diff --git a/apps/web/src/routes/_app/$orgSlug/settings/debug.tsx b/apps/web/src/routes/_app/$orgSlug/settings/debug.tsx index e834907b4..895ab37ef 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/debug.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/debug.tsx @@ -101,7 +101,7 @@ function DebugSettings() { const handleSyntheticNotification = () => { const now = new Date() - const syntheticNotification: typeof Notification.Model.Type = { + const syntheticNotification: Notification.Type = { id: `synthetic-${Date.now()}` as any, memberId: `synthetic-member-${Date.now()}` as any, targetedResourceId: null, diff --git a/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx b/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx index d727e72f6..aa6bfbc4d 100644 --- a/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx +++ b/apps/web/src/routes/_app/$orgSlug/settings/integrations/$integrationId.tsx @@ -554,7 +554,7 @@ function ConnectedState({ isConfiguring, }: { integration: Integration - connection: typeof IntegrationConnection.Model.Type | null + connection: IntegrationConnection.Type | null externalAccountName: string | null isDisconnecting: boolean onDisconnect: () => void diff --git a/apps/web/src/utils/attachment-url.ts b/apps/web/src/utils/attachment-url.ts index 2eba5d80f..9b2e24b34 100644 --- a/apps/web/src/utils/attachment-url.ts +++ b/apps/web/src/utils/attachment-url.ts @@ -3,5 +3,5 @@ import type { Attachment } from "@hazel/domain/models" const FALLBACK_PUBLIC_URL = import.meta.env.VITE_R2_PUBLIC_URL || "https://cdn.hazel.sh" export const getAttachmentUrl = ( - attachment: Pick, + attachment: Pick, ): string => attachment.externalUrl?.trim() || `${FALLBACK_PUBLIC_URL}/${attachment.id}` diff --git a/libs/bot-sdk/src/hazel-bot-sdk.ts b/libs/bot-sdk/src/hazel-bot-sdk.ts index ca9349e3c..bc7163a1c 100644 --- a/libs/bot-sdk/src/hazel-bot-sdk.ts +++ b/libs/bot-sdk/src/hazel-bot-sdk.ts @@ -134,9 +134,9 @@ export class HazelBotRuntimeConfigTag extends ServiceMap.Service< /** * Hazel-specific type aliases for convenience */ -export type MessageType = Schema.Schema.Type -export type ChannelType = Schema.Schema.Type -export type ChannelMemberType = Schema.Schema.Type +export type MessageType = Schema.Schema.Type +export type ChannelType = Schema.Schema.Type +export type ChannelMemberType = Schema.Schema.Type /** * Hazel-specific event handlers diff --git a/libs/effect-electric-db-collection/src/collection.ts b/libs/effect-electric-db-collection/src/collection.ts index af6511e4c..560487d1e 100644 --- a/libs/effect-electric-db-collection/src/collection.ts +++ b/libs/effect-electric-db-collection/src/collection.ts @@ -421,7 +421,7 @@ export type EffectCollection< * id: "messages", * runtime: runtime, * shapeOptions: { url: electricUrl, params: { table: "messages" } }, - * schema: Message.Model.json, // Direct Effect Schema! + * schema: Message.Schema, // Direct Effect Schema! * getKey: (item) => item.id, * onInsert: ({ transaction }) => Effect.gen(function* () { ... }), * }) diff --git a/packages/backend-core/src/repositories/attachment-repo.ts b/packages/backend-core/src/repositories/attachment-repo.ts index 8c59adb57..30f3a24c6 100644 --- a/packages/backend-core/src/repositories/attachment-repo.ts +++ b/packages/backend-core/src/repositories/attachment-repo.ts @@ -1,10 +1,10 @@ -import { ModelRepository, schema } from "@hazel/db" +import { Repository, schema } from "@hazel/db" import { Attachment } from "@hazel/domain/models" import { ServiceMap, Effect, Layer } from "effect" export class AttachmentRepo extends ServiceMap.Service()("AttachmentRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository(schema.attachmentsTable, Attachment.Model, { + const baseRepo = yield* Repository.makeRepository(schema.attachmentsTable, { insert: Attachment.Insert, update: Attachment.Update }, { idColumn: "id", name: "Attachment", }) diff --git a/packages/backend-core/src/repositories/bot-command-repo.ts b/packages/backend-core/src/repositories/bot-command-repo.ts index b3d881534..4f8b812b0 100644 --- a/packages/backend-core/src/repositories/bot-command-repo.ts +++ b/packages/backend-core/src/repositories/bot-command-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, inArray, lt, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, inArray, lt, Repository, schema, type TxFn } from "@hazel/db" import type { BotCommandId, BotId } from "@hazel/schema" import { BotCommand } from "@hazel/domain/models" @@ -6,7 +6,7 @@ import { ServiceMap, Effect, Layer, Option } from "effect" export class BotCommandRepo extends ServiceMap.Service()("BotCommandRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository(schema.botCommandsTable, BotCommand.Model, { + const baseRepo = yield* Repository.makeRepository(schema.botCommandsTable, { insert: BotCommand.Insert, update: BotCommand.Update }, { idColumn: "id", name: "BotCommand", }) diff --git a/packages/backend-core/src/repositories/bot-installation-repo.ts b/packages/backend-core/src/repositories/bot-installation-repo.ts index 7a9441a9c..7a01200cf 100644 --- a/packages/backend-core/src/repositories/bot-installation-repo.ts +++ b/packages/backend-core/src/repositories/bot-installation-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, inArray, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, inArray, Repository, schema, type TxFn } from "@hazel/db" import type { BotId, BotInstallationId, OrganizationId } from "@hazel/schema" import { BotInstallation } from "@hazel/domain/models" @@ -6,9 +6,9 @@ import { ServiceMap, Effect, Layer, Option } from "effect" export class BotInstallationRepo extends ServiceMap.Service()("BotInstallationRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository( + const baseRepo = yield* Repository.makeRepository( schema.botInstallationsTable, - BotInstallation.Model, + { insert: BotInstallation.Insert, update: BotInstallation.Update }, { idColumn: "id", name: "BotInstallation", diff --git a/packages/backend-core/src/repositories/bot-repo.ts b/packages/backend-core/src/repositories/bot-repo.ts index 5c64cd98a..d9637ff58 100644 --- a/packages/backend-core/src/repositories/bot-repo.ts +++ b/packages/backend-core/src/repositories/bot-repo.ts @@ -5,7 +5,7 @@ import { ilike, inArray, isNull, - ModelRepository, + Repository, or, schema, sql, @@ -18,7 +18,7 @@ import { ServiceMap, Effect, Layer, Option, type Schema } from "effect" export class BotRepo extends ServiceMap.Service()("BotRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository(schema.botsTable, Bot.Model, { + const baseRepo = yield* Repository.makeRepository(schema.botsTable, { insert: Bot.Insert, update: Bot.Update }, { idColumn: "id", name: "Bot", }) diff --git a/packages/backend-core/src/repositories/channel-member-repo.ts b/packages/backend-core/src/repositories/channel-member-repo.ts index 978de3e98..f17bbbbbd 100644 --- a/packages/backend-core/src/repositories/channel-member-repo.ts +++ b/packages/backend-core/src/repositories/channel-member-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, inArray, isNull, ModelRepository, schema, sql, type TxFn } from "@hazel/db" +import { and, Database, eq, inArray, isNull, Repository, schema, sql, type TxFn } from "@hazel/db" import type { ChannelId, OrganizationId, UserId } from "@hazel/schema" import { ChannelMember } from "@hazel/domain/models" @@ -6,9 +6,9 @@ import { ServiceMap, Effect, Layer, Option } from "effect" export class ChannelMemberRepo extends ServiceMap.Service()("ChannelMemberRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository( + const baseRepo = yield* Repository.makeRepository( schema.channelMembersTable, - ChannelMember.Model, + { insert: ChannelMember.Insert, update: ChannelMember.Update }, { idColumn: "id", name: "ChannelMember", diff --git a/packages/backend-core/src/repositories/channel-repo.ts b/packages/backend-core/src/repositories/channel-repo.ts index 3acab47a5..7eab89ad6 100644 --- a/packages/backend-core/src/repositories/channel-repo.ts +++ b/packages/backend-core/src/repositories/channel-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, isNull, Repository, schema, type TxFn } from "@hazel/db" import type { OrganizationId } from "@hazel/schema" import { Channel } from "@hazel/domain/models" @@ -6,7 +6,7 @@ import { ServiceMap, Effect, Layer, Option } from "effect" export class ChannelRepo extends ServiceMap.Service()("ChannelRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository(schema.channelsTable, Channel.Model, { + const baseRepo = yield* Repository.makeRepository(schema.channelsTable, { insert: Channel.Insert, update: Channel.Update }, { idColumn: "id", name: "Channel", }) diff --git a/packages/backend-core/src/repositories/channel-section-repo.ts b/packages/backend-core/src/repositories/channel-section-repo.ts index c21dc37cd..35152266c 100644 --- a/packages/backend-core/src/repositories/channel-section-repo.ts +++ b/packages/backend-core/src/repositories/channel-section-repo.ts @@ -1,12 +1,12 @@ -import { ModelRepository, schema } from "@hazel/db" +import { Repository, schema } from "@hazel/db" import { ChannelSection } from "@hazel/domain/models" import { ServiceMap, Effect, Layer } from "effect" export class ChannelSectionRepo extends ServiceMap.Service()("ChannelSectionRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository( + const baseRepo = yield* Repository.makeRepository( schema.channelSectionsTable, - ChannelSection.Model, + { insert: ChannelSection.Insert, update: ChannelSection.Update }, { idColumn: "id", name: "ChannelSection", diff --git a/packages/backend-core/src/repositories/channel-webhook-repo.ts b/packages/backend-core/src/repositories/channel-webhook-repo.ts index 555e552f7..dee2e2c11 100644 --- a/packages/backend-core/src/repositories/channel-webhook-repo.ts +++ b/packages/backend-core/src/repositories/channel-webhook-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, isNull, Repository, schema, type TxFn } from "@hazel/db" import type { ChannelId, ChannelWebhookId, OrganizationId } from "@hazel/schema" import { ChannelWebhook } from "@hazel/domain/models" @@ -6,9 +6,9 @@ import { ServiceMap, Effect, Layer, Option } from "effect" export class ChannelWebhookRepo extends ServiceMap.Service()("ChannelWebhookRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository( + const baseRepo = yield* Repository.makeRepository( schema.channelWebhooksTable, - ChannelWebhook.Model, + { insert: ChannelWebhook.Insert, update: ChannelWebhook.Update }, { idColumn: "id", name: "ChannelWebhook", diff --git a/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts b/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts index b04108f06..5e84be646 100644 --- a/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-channel-link-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, isNull, Repository, schema, type TxFn } from "@hazel/db" import { ChatSyncChannelLink } from "@hazel/domain/models" import type { ChannelId, ExternalChannelId, SyncChannelLinkId, SyncConnectionId } from "@hazel/schema" @@ -8,16 +8,16 @@ export class ChatSyncChannelLinkRepo extends ServiceMap.Service rows.map((row) => decodeChannelLink(row)) const decodeChannelLinkOption = (value: Option.Option) => diff --git a/packages/backend-core/src/repositories/chat-sync-connection-repo.ts b/packages/backend-core/src/repositories/chat-sync-connection-repo.ts index 0aecfaf09..478368109 100644 --- a/packages/backend-core/src/repositories/chat-sync-connection-repo.ts +++ b/packages/backend-core/src/repositories/chat-sync-connection-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, isNull, Repository, schema, type TxFn } from "@hazel/db" import { ChatSyncConnection } from "@hazel/domain/models" import type { IntegrationConnectionId, OrganizationId, SyncConnectionId } from "@hazel/schema" @@ -8,9 +8,9 @@ export class ChatSyncConnectionRepo extends ServiceMap.Service rows.map((row) => decodeMessageLink(row)) const decodeMessageLinkOption = (value: Option.Option) => diff --git a/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts b/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts index 7aa6fa791..f5cfeef29 100644 --- a/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts +++ b/packages/backend-core/src/repositories/connect-conversation-channel-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, isNull, Repository, schema, type TxFn } from "@hazel/db" import type { ChannelId, ConnectConversationId, OrganizationId } from "@hazel/schema" import { ConnectConversationChannel } from "@hazel/domain/models" import { ServiceMap, Effect, Layer, Option } from "effect" @@ -7,9 +7,9 @@ export class ConnectConversationChannelRepo extends ServiceMap.Service()("ConnectInviteRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository( + const baseRepo = yield* Repository.makeRepository( schema.connectInvitesTable, - ConnectInvite.Model, + { insert: ConnectInvite.Insert, update: ConnectInvite.Update }, { idColumn: "id", name: "ConnectInvite", diff --git a/packages/backend-core/src/repositories/connect-participant-repo.ts b/packages/backend-core/src/repositories/connect-participant-repo.ts index 09fc4ed45..fb4fb910b 100644 --- a/packages/backend-core/src/repositories/connect-participant-repo.ts +++ b/packages/backend-core/src/repositories/connect-participant-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, isNull, Repository, schema, type TxFn } from "@hazel/db" import type { ChannelId, ConnectConversationId, UserId } from "@hazel/schema" import { ConnectParticipant } from "@hazel/domain/models" import { ServiceMap, Effect, Layer, Option, type Schema as EffectSchema } from "effect" @@ -7,9 +7,9 @@ export class ConnectParticipantRepo extends ServiceMap.Service()("CustomEmojiRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository(schema.customEmojisTable, CustomEmoji.Model, { + const baseRepo = yield* Repository.makeRepository(schema.customEmojisTable, { insert: CustomEmoji.Insert, update: CustomEmoji.Update }, { idColumn: "id", name: "CustomEmoji", }) diff --git a/packages/backend-core/src/repositories/github-subscription-repo.ts b/packages/backend-core/src/repositories/github-subscription-repo.ts index d4a0d0d65..40a7f61a4 100644 --- a/packages/backend-core/src/repositories/github-subscription-repo.ts +++ b/packages/backend-core/src/repositories/github-subscription-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, isNull, Repository, schema, type TxFn } from "@hazel/db" import type { ChannelId, GitHubSubscriptionId, OrganizationId } from "@hazel/schema" import { GitHubSubscription } from "@hazel/domain/models" @@ -8,9 +8,9 @@ export class GitHubSubscriptionRepo extends ServiceMap.Service()("IntegrationTokenRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository( + const baseRepo = yield* Repository.makeRepository( schema.integrationTokensTable, - IntegrationToken.Model, + { insert: IntegrationToken.Insert, update: IntegrationToken.Update }, { idColumn: "id", name: "IntegrationToken", diff --git a/packages/backend-core/src/repositories/invitation-repo.ts b/packages/backend-core/src/repositories/invitation-repo.ts index 6be91ec46..7e1e0e0ee 100644 --- a/packages/backend-core/src/repositories/invitation-repo.ts +++ b/packages/backend-core/src/repositories/invitation-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, lte, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, lte, Repository, schema, type TxFn } from "@hazel/db" import type { InvitationId, OrganizationId, WorkOSInvitationId } from "@hazel/schema" import { Invitation } from "@hazel/domain/models" @@ -6,7 +6,7 @@ import { ServiceMap, Effect, Layer, Option, type Schema } from "effect" export class InvitationRepo extends ServiceMap.Service()("InvitationRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository(schema.invitationsTable, Invitation.Model, { + const baseRepo = yield* Repository.makeRepository(schema.invitationsTable, { insert: Invitation.Insert, update: Invitation.Update }, { idColumn: "id", name: "Invitation", }) diff --git a/packages/backend-core/src/repositories/message-reaction-repo.ts b/packages/backend-core/src/repositories/message-reaction-repo.ts index 90a08c79a..66e0cec07 100644 --- a/packages/backend-core/src/repositories/message-reaction-repo.ts +++ b/packages/backend-core/src/repositories/message-reaction-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, isNull, Repository, schema, type TxFn } from "@hazel/db" import type { ChannelId, ConnectConversationId, MessageId, UserId } from "@hazel/schema" import { MessageReaction } from "@hazel/domain/models" @@ -6,9 +6,9 @@ import { ServiceMap, Effect, Layer, Option } from "effect" export class MessageReactionRepo extends ServiceMap.Service()("MessageReactionRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository( + const baseRepo = yield* Repository.makeRepository( schema.messageReactionsTable, - MessageReaction.Model, + { insert: MessageReaction.Insert, update: MessageReaction.Update }, { idColumn: "id", name: "MessageReaction", diff --git a/packages/backend-core/src/repositories/message-repo.ts b/packages/backend-core/src/repositories/message-repo.ts index dd1e0c4ff..a81725aff 100644 --- a/packages/backend-core/src/repositories/message-repo.ts +++ b/packages/backend-core/src/repositories/message-repo.ts @@ -8,7 +8,7 @@ import { inArray, isNull, lt, - ModelRepository, + Repository, schema, sql, type TxFn, @@ -37,7 +37,7 @@ export interface ListByChannelParams { export class MessageRepo extends ServiceMap.Service()("MessageRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository(schema.messagesTable, Message.Model, { + const baseRepo = yield* Repository.makeRepository(schema.messagesTable, { insert: Message.Insert, update: Message.Update }, { idColumn: "id", name: "Message", }) diff --git a/packages/backend-core/src/repositories/notification-repo.ts b/packages/backend-core/src/repositories/notification-repo.ts index 1f8e64ef7..a2f588b37 100644 --- a/packages/backend-core/src/repositories/notification-repo.ts +++ b/packages/backend-core/src/repositories/notification-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, inArray, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, inArray, Repository, schema, type TxFn } from "@hazel/db" import type { ChannelId, MessageId, OrganizationMemberId } from "@hazel/schema" import { Notification } from "@hazel/domain/models" @@ -6,9 +6,9 @@ import { ServiceMap, Effect, Layer } from "effect" export class NotificationRepo extends ServiceMap.Service()("NotificationRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository( + const baseRepo = yield* Repository.makeRepository( schema.notificationsTable, - Notification.Model, + { insert: Notification.Insert, update: Notification.Update }, { idColumn: "id", name: "Notification", diff --git a/packages/backend-core/src/repositories/organization-member-repo.ts b/packages/backend-core/src/repositories/organization-member-repo.ts index bf7dc2957..67879b649 100644 --- a/packages/backend-core/src/repositories/organization-member-repo.ts +++ b/packages/backend-core/src/repositories/organization-member-repo.ts @@ -1,4 +1,4 @@ -import { and, count, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, count, Database, eq, isNull, Repository, schema, type TxFn } from "@hazel/db" import type { OrganizationId, OrganizationMemberId, UserId } from "@hazel/schema" import { OrganizationMember } from "@hazel/domain/models" @@ -8,9 +8,9 @@ export class OrganizationMemberRepo extends ServiceMap.Service()("OrganizationRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository( + const baseRepo = yield* Repository.makeRepository( schema.organizationsTable, - Organization.Model, + { insert: Organization.Insert, update: Organization.Update }, { idColumn: "id", name: "Organization", diff --git a/packages/backend-core/src/repositories/pinned-message-repo.ts b/packages/backend-core/src/repositories/pinned-message-repo.ts index 2f0fc1da3..4ce5d4ff9 100644 --- a/packages/backend-core/src/repositories/pinned-message-repo.ts +++ b/packages/backend-core/src/repositories/pinned-message-repo.ts @@ -1,12 +1,12 @@ -import { ModelRepository, schema } from "@hazel/db" +import { Repository, schema } from "@hazel/db" import { PinnedMessage } from "@hazel/domain/models" import { ServiceMap, Effect, Layer } from "effect" export class PinnedMessageRepo extends ServiceMap.Service()("PinnedMessageRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository( + const baseRepo = yield* Repository.makeRepository( schema.pinnedMessagesTable, - PinnedMessage.Model, + { insert: PinnedMessage.Insert, update: PinnedMessage.Update }, { idColumn: "id", name: "PinnedMessage", diff --git a/packages/backend-core/src/repositories/rss-subscription-repo.ts b/packages/backend-core/src/repositories/rss-subscription-repo.ts index d6944eba9..6775670b1 100644 --- a/packages/backend-core/src/repositories/rss-subscription-repo.ts +++ b/packages/backend-core/src/repositories/rss-subscription-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, isNull, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, isNull, Repository, schema, type TxFn } from "@hazel/db" import type { ChannelId, OrganizationId, RssSubscriptionId } from "@hazel/schema" import { RssSubscription } from "@hazel/domain/models" @@ -6,9 +6,9 @@ import { ServiceMap, Effect, Layer, Option } from "effect" export class RssSubscriptionRepo extends ServiceMap.Service()("RssSubscriptionRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository( + const baseRepo = yield* Repository.makeRepository( schema.rssSubscriptionsTable, - RssSubscription.Model, + { insert: RssSubscription.Insert, update: RssSubscription.Update }, { idColumn: "id", name: "RssSubscription", diff --git a/packages/backend-core/src/repositories/typing-indicator-repo.ts b/packages/backend-core/src/repositories/typing-indicator-repo.ts index afdfa6202..64d572427 100644 --- a/packages/backend-core/src/repositories/typing-indicator-repo.ts +++ b/packages/backend-core/src/repositories/typing-indicator-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, lt, ModelRepository, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, lt, Repository, schema, type TxFn } from "@hazel/db" import { ChannelId, ChannelMemberId, TypingIndicatorId } from "@hazel/schema" import { TypingIndicator } from "@hazel/domain/models" @@ -7,9 +7,9 @@ import { ServiceMap, Effect, Layer } from "effect" export class TypingIndicatorRepo extends ServiceMap.Service()("TypingIndicatorRepo", { make: Effect.gen(function* () { const db = yield* Database.Database - const baseRepo = yield* ModelRepository.makeRepository( + const baseRepo = yield* Repository.makeRepository( schema.typingIndicatorsTable, - TypingIndicator.Model, + { insert: TypingIndicator.Insert, update: TypingIndicator.Update }, { idColumn: "id", name: "TypingIndicator", diff --git a/packages/backend-core/src/repositories/user-presence-status-repo.ts b/packages/backend-core/src/repositories/user-presence-status-repo.ts index edd2ceec3..d59053c7a 100644 --- a/packages/backend-core/src/repositories/user-presence-status-repo.ts +++ b/packages/backend-core/src/repositories/user-presence-status-repo.ts @@ -1,4 +1,4 @@ -import { and, Database, eq, inArray, lt, ModelRepository, ne, schema, type TxFn } from "@hazel/db" +import { and, Database, eq, inArray, lt, Repository, ne, schema, type TxFn } from "@hazel/db" import type { ChannelId, UserId } from "@hazel/schema" import { UserPresenceStatus } from "@hazel/domain/models" @@ -9,9 +9,9 @@ export class UserPresenceStatusRepo extends ServiceMap.Service()("UserRepo", { make: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository(schema.usersTable, User.Model, { + const baseRepo = yield* Repository.makeRepository(schema.usersTable, { insert: User.Insert, update: User.Update }, { idColumn: "id", name: "User", }) diff --git a/packages/db/README.md b/packages/db/README.md index f336f954a..23bdb485b 100644 --- a/packages/db/README.md +++ b/packages/db/README.md @@ -182,16 +182,21 @@ Effect.gen(function* () { ### Creating a Repository ```typescript -import { ModelRepository, schema } from "@hazel/db" +import { Repository, schema } from "@hazel/db" +import { User } from "@hazel/domain/models" import { Effect } from "effect" export class UserRepo extends Effect.Service()("UserRepo", { accessors: true, effect: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository(schema.usersTable, User.Model, { - idColumn: "id", - name: "User", - }) + const baseRepo = yield* Repository.makeRepository( + schema.usersTable, + { insert: User.Insert, update: User.Update }, + { + idColumn: "id", + name: "User", + }, + ) return baseRepo }), @@ -217,7 +222,7 @@ All repositories include these methods: export class ChannelMemberRepo extends Effect.Service()("ChannelMemberRepo", { accessors: true, effect: Effect.gen(function* () { - const baseRepo = yield* ModelRepository.makeRepository(/*...*/) + const baseRepo = yield* Repository.makeRepository(/*...*/) const db = yield* Database.Database // Custom method with automatic transaction support diff --git a/packages/db/src/services/index.ts b/packages/db/src/services/index.ts index 2430651de..cd7026072 100644 --- a/packages/db/src/services/index.ts +++ b/packages/db/src/services/index.ts @@ -1,5 +1,4 @@ export type { DatabaseError, TransactionClient, TxFn } from "./database" export * as Database from "./database" export * as DrizzleEffect from "./drizzle-effect" -export * as Model from "./model" -export * as ModelRepository from "./model-repository" +export * as Repository from "./repository" diff --git a/packages/db/src/services/model-repository.ts b/packages/db/src/services/model-repository.ts deleted file mode 100644 index d0e74b555..000000000 --- a/packages/db/src/services/model-repository.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { InferSelectModel, Table } from "drizzle-orm" -import { eq } from "drizzle-orm" -import { pipe, Struct } from "effect" -import * as Effect from "effect/Effect" -import * as Option from "effect/Option" -import * as Schema from "effect/Schema" -import { Database, type DatabaseError, type TxFn } from "./database" -import { EntityNotFound, type EntitySchema, type Repository, type RepositoryOptions } from "./model" - -export function makeRepository< - T extends Table, - Col extends keyof InferSelectModel, - Name extends string, - RecordType extends InferSelectModel, - S extends EntitySchema, - Id extends InferSelectModel[Col], ->( - table: T, - schema: S, - options: RepositoryOptions, -): Effect.Effect, never, Database> { - return Effect.gen(function* () { - const db = yield* Database - const { idColumn } = options - - const insert = (data: S["insert"]["Type"], tx?: TxFn) => - pipe( - db.makeQueryWithSchema(schema.insert as Schema.Top, (execute, input: any) => - execute((client) => client.insert(table).values([input]).returning()), - )(data, tx), - ) as unknown as Effect.Effect - - const insertVoid = (data: S["insert"]["Type"], tx?: TxFn) => - db.makeQueryWithSchema(schema.insert as Schema.Top, (execute, input: any) => - execute((client) => client.insert(table).values(input)), - )(data, tx) as unknown as Effect.Effect - - const update = (data: S["update"]["Type"], tx?: TxFn) => - db.makeQueryWithSchema( - (schema.update as Schema.Struct).mapFields(Struct.map(Schema.optional)) as Schema.Top, - (execute, input: any) => - execute((client) => - client - .update(table) - .set(input) - // @ts-expect-error - .where(eq(table[idColumn], input[idColumn])) - .returning(), - ).pipe( - Effect.flatMap((result) => - result.length > 0 - ? Effect.succeed(result[0] as RecordType) - : Effect.die(new EntityNotFound({ type: options.name, id: input[idColumn] })), - ), - ), - )(data, tx) as Effect.Effect - - const updateVoid = (data: S["update"]["Type"], tx?: TxFn) => - db.makeQueryWithSchema( - (schema.update as Schema.Struct).mapFields(Struct.map(Schema.optional)) as Schema.Top, - (execute, input: any) => - execute((client) => - client - .update(table) - .set(input) - // @ts-expect-error - .where(eq(table[idColumn], input[idColumn])), - ), - )(data, tx) as unknown as Effect.Effect - - const findById = (id: Id, tx?: TxFn) => - db.makeQuery((execute, id: Id) => - execute((client) => - client - .select() - .from(table as Table) - // @ts-expect-error - .where(eq(table[idColumn], id)) - .limit(1), - ).pipe(Effect.map((results) => Option.fromNullishOr(results[0] as RecordType))), - )(id, tx) as Effect.Effect, DatabaseError> - - const deleteById = (id: Id, tx?: TxFn) => - db.makeQuery((execute, id: Id) => - // @ts-expect-error - execute((client) => client.delete(table).where(eq(table[idColumn], id))), - )(id, tx) as Effect.Effect - - const with_ = ( - id: Id, - f: (item: RecordType) => Effect.Effect, - ): Effect.Effect => - pipe( - findById(id), - Effect.flatMap( - Option.match({ - onNone: () => Effect.fail(new EntityNotFound({ type: options.name, id })), - onSome: Effect.succeed, - }), - ), - Effect.flatMap(f), - Effect.catchTag("DatabaseError", (err) => Effect.die(err)), - ) - - return { - insert, - insertVoid, - update, - updateVoid, - findById, - deleteById, - with: with_, - } - }) -} diff --git a/packages/db/src/services/model.ts b/packages/db/src/services/model.ts deleted file mode 100644 index b7ae163ec..000000000 --- a/packages/db/src/services/model.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Re-export model utilities from domain and add db-specific Repository types. - */ -import * as Model from "@hazel/domain/models" - -export * from "@hazel/domain/models" - -import type { EntitySchema } from "@hazel/domain/models" -import type * as Effect from "effect/Effect" -import type * as Option from "effect/Option" -import * as Schema from "effect/Schema" -import type { DatabaseError, TransactionClient } from "./database" - -export interface RepositoryOptions { - idColumn: Col - name: Name -} - -export type PartialExcept = Partial> & Pick - -export class EntityNotFound extends Schema.TaggedErrorClass()("EntityNotFound", { - type: Schema.String, - id: Schema.Any, -}) {} - -export interface Repository< - RecordType, - S extends EntitySchema, - Col extends string & keyof S["update"]["Type"], - Name extends string, - Id, -> { - readonly insert: ( - insert: S["insert"]["Type"], - tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, - ) => Effect.Effect - - readonly insertVoid: ( - insert: S["insert"]["Type"], - tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, - ) => Effect.Effect - - readonly update: ( - update: PartialExcept, - tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, - ) => Effect.Effect - - readonly updateVoid: ( - update: PartialExcept, - tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, - ) => Effect.Effect - - // readonly updateManyVoid: ( - // update: PartialExcept[] - // ) => Effect.Effect - - readonly findById: ( - id: Id, - tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, - ) => Effect.Effect, DatabaseError> - - readonly with: ( - id: Id, - f: (item: RecordType) => Effect.Effect, - ) => Effect.Effect - - readonly deleteById: ( - id: Id, - tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, - ) => Effect.Effect -} diff --git a/packages/db/src/services/repository.ts b/packages/db/src/services/repository.ts new file mode 100644 index 000000000..28f3e5cd9 --- /dev/null +++ b/packages/db/src/services/repository.ts @@ -0,0 +1,175 @@ +import type { InferSelectModel, Table } from "drizzle-orm" +import { eq } from "drizzle-orm" +import { pipe, Struct } from "effect" +import * as Effect from "effect/Effect" +import * as Option from "effect/Option" +import * as Schema from "effect/Schema" +import { Database, type DatabaseError, type TransactionClient, type TxFn } from "./database" + +export interface RepositoryOptions { + idColumn: Col + name: Name +} + +export type PartialExcept = Partial> & Pick + +export class EntityNotFound extends Schema.TaggedErrorClass()("EntityNotFound", { + type: Schema.String, + id: Schema.Any, +}) {} + +export interface RepositorySchemas { + readonly insert: InsertSchema + readonly update: UpdateSchema +} + +export interface Repository< + RecordType, + InsertType, + UpdateType, + Col extends keyof UpdateType & string, + Name extends string, + Id, +> { + readonly insert: ( + insert: InsertType, + tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, + ) => Effect.Effect + + readonly insertVoid: ( + insert: InsertType, + tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, + ) => Effect.Effect + + readonly update: ( + update: PartialExcept, + tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, + ) => Effect.Effect + + readonly updateVoid: ( + update: PartialExcept, + tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, + ) => Effect.Effect + + readonly findById: ( + id: Id, + tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, + ) => Effect.Effect, DatabaseError> + + readonly with: ( + id: Id, + f: (item: RecordType) => Effect.Effect, + ) => Effect.Effect + + readonly deleteById: ( + id: Id, + tx?: (fn: (client: TransactionClient) => Promise) => Effect.Effect, + ) => Effect.Effect +} + +export function makeRepository< + T extends Table, + InsertSchema extends Schema.Top, + UpdateSchema extends Schema.Top, + Col extends keyof InferSelectModel & keyof UpdateSchema["Type"] & string, + Name extends string, + RecordType extends InferSelectModel, + Id extends InferSelectModel[Col], +>( + table: T, + schemas: RepositorySchemas, + options: RepositoryOptions, +): Effect.Effect, never, Database> { + return Effect.gen(function* () { + const db = yield* Database + const { idColumn } = options + const updateSchema = ((schemas.update as unknown) as Schema.Struct).mapFields( + Struct.map(Schema.optional), + ) as Schema.Top + + const insert = (data: InsertSchema["Type"], tx?: TxFn) => + pipe( + db.makeQueryWithSchema(schemas.insert as Schema.Top, (execute, input: any) => + execute((client) => client.insert(table).values([input]).returning()), + )(data, tx), + ) as unknown as Effect.Effect + + const insertVoid = (data: InsertSchema["Type"], tx?: TxFn) => + db.makeQueryWithSchema(schemas.insert as Schema.Top, (execute, input: any) => + execute((client) => client.insert(table).values(input)), + )(data, tx) as unknown as Effect.Effect + + const update = (data: PartialExcept, tx?: TxFn) => + db.makeQueryWithSchema(updateSchema, (execute, input: any) => + execute((client) => + client + .update(table) + .set(input) + // @ts-expect-error drizzle column access is runtime-indexed by configured idColumn + .where(eq(table[idColumn], input[idColumn])) + .returning(), + ).pipe( + Effect.flatMap((result) => + result.length > 0 + ? Effect.succeed(result[0] as RecordType) + : Effect.die(new EntityNotFound({ type: options.name, id: input[idColumn] })), + ), + ), + )(data, tx) as Effect.Effect + + const updateVoid = (data: PartialExcept, tx?: TxFn) => + db.makeQueryWithSchema(updateSchema, (execute, input: any) => + execute((client) => + client + .update(table) + .set(input) + // @ts-expect-error drizzle column access is runtime-indexed by configured idColumn + .where(eq(table[idColumn], input[idColumn])), + ), + )(data, tx) as unknown as Effect.Effect + + const findById = (id: Id, tx?: TxFn) => + db.makeQuery((execute, inputId: Id) => + execute((client) => + client + .select() + .from(table as Table) + // @ts-expect-error drizzle column access is runtime-indexed by configured idColumn + .where(eq(table[idColumn], inputId)) + .limit(1), + ).pipe(Effect.map((results) => Option.fromNullishOr(results[0] as RecordType))), + )(id, tx) as Effect.Effect, DatabaseError> + + const deleteById = (id: Id, tx?: TxFn) => + db.makeQuery((execute, inputId: Id) => + // @ts-expect-error drizzle column access is runtime-indexed by configured idColumn + execute((client) => client.delete(table).where(eq(table[idColumn], inputId))), + )(id, tx) as Effect.Effect + + const with_ = ( + id: Id, + f: (item: RecordType) => Effect.Effect, + ): Effect.Effect => + pipe( + findById(id), + Effect.flatMap( + Option.match({ + onNone: () => Effect.fail(new EntityNotFound({ type: options.name, id })), + onSome: Effect.succeed, + }), + ), + Effect.flatMap(f), + Effect.catchTag("DatabaseError", (err) => Effect.die(err)), + ) + + return { + insert, + insertVoid, + update, + updateVoid, + findById, + deleteById, + with: with_, + } + }) +} diff --git a/packages/domain/src/bot-gateway.ts b/packages/domain/src/bot-gateway.ts index 656dc7d14..acff67935 100644 --- a/packages/domain/src/bot-gateway.ts +++ b/packages/domain/src/bot-gateway.ts @@ -31,49 +31,49 @@ export const BotGatewayCommandInvokeEnvelope = Schema.Struct({ export const BotGatewayMessageCreateEnvelope = Schema.Struct({ ...BaseGatewayEnvelope, eventType: Schema.Literal("message.create"), - payload: Message.Model.json, + payload: Message.Schema, }) export const BotGatewayMessageUpdateEnvelope = Schema.Struct({ ...BaseGatewayEnvelope, eventType: Schema.Literal("message.update"), - payload: Message.Model.json, + payload: Message.Schema, }) export const BotGatewayMessageDeleteEnvelope = Schema.Struct({ ...BaseGatewayEnvelope, eventType: Schema.Literal("message.delete"), - payload: Message.Model.json, + payload: Message.Schema, }) export const BotGatewayChannelCreateEnvelope = Schema.Struct({ ...BaseGatewayEnvelope, eventType: Schema.Literal("channel.create"), - payload: Channel.Model.json, + payload: Channel.Schema, }) export const BotGatewayChannelUpdateEnvelope = Schema.Struct({ ...BaseGatewayEnvelope, eventType: Schema.Literal("channel.update"), - payload: Channel.Model.json, + payload: Channel.Schema, }) export const BotGatewayChannelDeleteEnvelope = Schema.Struct({ ...BaseGatewayEnvelope, eventType: Schema.Literal("channel.delete"), - payload: Channel.Model.json, + payload: Channel.Schema, }) export const BotGatewayChannelMemberAddEnvelope = Schema.Struct({ ...BaseGatewayEnvelope, eventType: Schema.Literal("channel_member.add"), - payload: ChannelMember.Model.json, + payload: ChannelMember.Schema, }) export const BotGatewayChannelMemberRemoveEnvelope = Schema.Struct({ ...BaseGatewayEnvelope, eventType: Schema.Literal("channel_member.remove"), - payload: ChannelMember.Model.json, + payload: ChannelMember.Schema, }) export const BotGatewayEnvelope = Schema.Union([ diff --git a/packages/domain/src/http/api-v1/messages.ts b/packages/domain/src/http/api-v1/messages.ts index 3026cdc9c..20de1a669 100644 --- a/packages/domain/src/http/api-v1/messages.ts +++ b/packages/domain/src/http/api-v1/messages.ts @@ -27,7 +27,7 @@ export class ListMessagesQuery extends Schema.Class("ListMess }) {} export class ListMessagesResponse extends Schema.Class("ListMessagesResponse")({ - data: Schema.Array(Message.Model.json as any), + data: Schema.Array(Message.Schema as any), has_more: Schema.Boolean, }) {} @@ -55,7 +55,7 @@ export class ToggleReactionRequest extends Schema.Class(" // ============ RESPONSE SCHEMAS ============ export class MessageResponse extends Schema.Class("MessageResponse")({ - data: Message.Model.json as any, + data: Message.Schema as any, transactionId: TransactionId, }) {} @@ -65,7 +65,7 @@ export class DeleteMessageResponse extends Schema.Class(" export class ToggleReactionResponse extends Schema.Class("ToggleReactionResponse")({ wasCreated: Schema.Boolean, - data: Schema.optional(MessageReaction.Model.json as any), + data: Schema.optional(MessageReaction.Schema as any), transactionId: TransactionId, }) {} diff --git a/packages/domain/src/http/chat-sync.ts b/packages/domain/src/http/chat-sync.ts index 008e77187..b58167e31 100644 --- a/packages/domain/src/http/chat-sync.ts +++ b/packages/domain/src/http/chat-sync.ts @@ -17,27 +17,27 @@ import { RequiredScopes } from "../scopes/required-scopes" export class ChatSyncConnectionResponse extends Schema.Class( "ChatSyncConnectionResponse", )({ - data: ChatSyncConnection.Model.json as any, + data: ChatSyncConnection.Schema as any, transactionId: TransactionId, }) {} export class ChatSyncConnectionListResponse extends Schema.Class( "ChatSyncConnectionListResponse", )({ - data: Schema.Array(ChatSyncConnection.Model.json as any), + data: Schema.Array(ChatSyncConnection.Schema as any), }) {} export class ChatSyncChannelLinkResponse extends Schema.Class( "ChatSyncChannelLinkResponse", )({ - data: ChatSyncChannelLink.Model.json as any, + data: ChatSyncChannelLink.Schema as any, transactionId: TransactionId, }) {} export class ChatSyncChannelLinkListResponse extends Schema.Class( "ChatSyncChannelLinkListResponse", )({ - data: Schema.Array(ChatSyncChannelLink.Model.json as any), + data: Schema.Array(ChatSyncChannelLink.Schema as any), }) {} export class ChatSyncDeleteResponse extends Schema.Class("ChatSyncDeleteResponse")({ diff --git a/packages/domain/src/models/attachment-model.ts b/packages/domain/src/models/attachment-model.ts index 34379d9fb..fa9bc8848 100644 --- a/packages/domain/src/models/attachment-model.ts +++ b/packages/domain/src/models/attachment-model.ts @@ -1,24 +1,24 @@ import { AttachmentId, ChannelId, MessageId, OrganizationId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const AttachmentStatus = Schema.Literals(["uploading", "complete", "failed"]) -export type AttachmentStatus = Schema.Schema.Type +export const AttachmentStatus = S.Literals(["uploading", "complete", "failed"]) +export type AttachmentStatus = S.Schema.Type -export class Model extends M.Class("Attachment")({ +class Model extends M.Class("Attachment")({ id: M.GeneratedByApp(AttachmentId), organizationId: OrganizationId, - channelId: Schema.NullOr(ChannelId), - messageId: Schema.NullOr(MessageId), - fileName: Schema.String, - fileSize: Schema.Number, - externalUrl: Schema.NullOr(Schema.String), + channelId: S.NullOr(ChannelId), + messageId: S.NullOr(MessageId), + fileName: S.String, + fileSize: S.Number, + externalUrl: S.NullOr(S.String), uploadedBy: M.GeneratedByApp(UserId), status: AttachmentStatus, uploadedAt: JsonDate, - deletedAt: M.Generated(Schema.NullOr(JsonDate)), + deletedAt: M.Generated(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/bot-command-model.ts b/packages/domain/src/models/bot-command-model.ts index fdd8dbfad..fdb4dfb14 100644 --- a/packages/domain/src/models/bot-command-model.ts +++ b/packages/domain/src/models/bot-command-model.ts @@ -1,31 +1,31 @@ import { BotCommandId, BotId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { Generated, JsonDate } from "./utils" /** * Argument definition for a bot command */ -export const BotCommandArgument = Schema.Struct({ - name: Schema.String, - description: Schema.NullOr(Schema.String), - required: Schema.Boolean, - placeholder: Schema.NullOr(Schema.String), - type: Schema.Literals(["string", "number", "user", "channel"]), +export const BotCommandArgument = S.Struct({ + name: S.String, + description: S.NullOr(S.String), + required: S.Boolean, + placeholder: S.NullOr(S.String), + type: S.Literals(["string", "number", "user", "channel"]), }) export type BotCommandArgument = typeof BotCommandArgument.Type -export class Model extends M.Class("BotCommand")({ +class Model extends M.Class("BotCommand")({ id: M.Generated(BotCommandId), botId: BotId, - name: Schema.String, - description: Schema.String, - arguments: Schema.NullOr(Schema.Array(BotCommandArgument)), - usageExample: Schema.NullOr(Schema.String), - isEnabled: Schema.Boolean, + name: S.String, + description: S.String, + arguments: S.NullOr(S.Array(BotCommandArgument)), + usageExample: S.NullOr(S.String), + isEnabled: S.Boolean, createdAt: Generated(JsonDate), - updatedAt: Generated(Schema.NullOr(JsonDate)), + updatedAt: Generated(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/bot-installation-model.ts b/packages/domain/src/models/bot-installation-model.ts index 75ffbf0eb..c4303edff 100644 --- a/packages/domain/src/models/bot-installation-model.ts +++ b/packages/domain/src/models/bot-installation-model.ts @@ -1,9 +1,9 @@ import { BotId, BotInstallationId, OrganizationId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { Generated, JsonDate } from "./utils" -export class Model extends M.Class("BotInstallation")({ +class Model extends M.Class("BotInstallation")({ id: M.Generated(BotInstallationId), botId: BotId, organizationId: OrganizationId, @@ -11,5 +11,5 @@ export class Model extends M.Class("BotInstallation")({ installedAt: Generated(JsonDate), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/bot-model.ts b/packages/domain/src/models/bot-model.ts index 67690891d..32762912d 100644 --- a/packages/domain/src/models/bot-model.ts +++ b/packages/domain/src/models/bot-model.ts @@ -1,27 +1,27 @@ import { BotId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import { IntegrationProvider } from "./integration-connection-model" import * as M from "./utils" import { baseFields, JsonDate } from "./utils" -export class Model extends M.Class("Bot")({ +class Model extends M.Class("Bot")({ id: M.Generated(BotId), userId: UserId, createdBy: UserId, - name: Schema.String, - description: Schema.NullOr(Schema.String), - webhookUrl: Schema.NullOr(Schema.String), - apiTokenHash: Schema.String, - scopes: Schema.NullOr(Schema.Array(Schema.String)), - metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), - isPublic: Schema.Boolean, - installCount: Schema.Number, + name: S.String, + description: S.NullOr(S.String), + webhookUrl: S.NullOr(S.String), + apiTokenHash: S.String, + scopes: S.NullOr(S.Array(S.String)), + metadata: S.NullOr(S.Record(S.String, S.Unknown)), + isPublic: S.Boolean, + installCount: S.Number, // List of integration providers this bot is allowed to use (e.g., ["linear", "github"]) - allowedIntegrations: Schema.NullOr(Schema.Array(IntegrationProvider)), + allowedIntegrations: S.NullOr(S.Array(IntegrationProvider)), // Whether this bot can be @mentioned in messages - mentionable: Schema.Boolean, + mentionable: S.Boolean, ...baseFields, }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/channel-member-model.ts b/packages/domain/src/models/channel-member-model.ts index 59faee83e..fbce145a5 100644 --- a/packages/domain/src/models/channel-member-model.ts +++ b/packages/domain/src/models/channel-member-model.ts @@ -1,21 +1,21 @@ import { ChannelId, ChannelMemberId, MessageId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export class Model extends M.Class("ChannelMember")({ +class Model extends M.Class("ChannelMember")({ id: M.Generated(ChannelMemberId), channelId: ChannelId, userId: M.GeneratedByApp(UserId), - isHidden: Schema.Boolean, - isMuted: Schema.Boolean, - isFavorite: Schema.Boolean, - lastSeenMessageId: Schema.NullOr(MessageId), - notificationCount: Schema.Number, + isHidden: S.Boolean, + isMuted: S.Boolean, + isFavorite: S.Boolean, + lastSeenMessageId: S.NullOr(MessageId), + notificationCount: S.Number, joinedAt: M.GeneratedByApp(JsonDate), createdAt: M.Generated(JsonDate), - deletedAt: M.GeneratedByApp(Schema.NullOr(JsonDate)), + deletedAt: M.GeneratedByApp(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/channel-model.ts b/packages/domain/src/models/channel-model.ts index 6989455bb..4603bc6f9 100644 --- a/packages/domain/src/models/channel-model.ts +++ b/packages/domain/src/models/channel-model.ts @@ -1,21 +1,21 @@ import { ChannelIcon, ChannelId, ChannelSectionId, OrganizationId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { baseFields } from "./utils" -export const ChannelType = Schema.Literals(["public", "private", "thread", "direct", "single"]) -export type ChannelType = Schema.Schema.Type +export const ChannelType = S.Literals(["public", "private", "thread", "direct", "single"]) +export type ChannelType = S.Schema.Type -export class Model extends M.Class("Channel")({ +class Model extends M.Class("Channel")({ id: M.GeneratedOptional(ChannelId), - name: Schema.String, - icon: Schema.NullOr(ChannelIcon), + name: S.String, + icon: S.NullOr(ChannelIcon), type: ChannelType, organizationId: OrganizationId, - parentChannelId: Schema.NullOr(ChannelId), - sectionId: Schema.NullOr(ChannelSectionId), + parentChannelId: S.NullOr(ChannelId), + sectionId: S.NullOr(ChannelSectionId), ...baseFields, }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/channel-section-model.ts b/packages/domain/src/models/channel-section-model.ts index 597b50b23..263a6627d 100644 --- a/packages/domain/src/models/channel-section-model.ts +++ b/packages/domain/src/models/channel-section-model.ts @@ -1,15 +1,15 @@ import { ChannelSectionId, OrganizationId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { baseFields } from "./utils" -export class Model extends M.Class("ChannelSection")({ +class Model extends M.Class("ChannelSection")({ id: M.GeneratedOptional(ChannelSectionId), organizationId: OrganizationId, - name: Schema.String, - order: Schema.Number, + name: S.String, + order: S.Number, ...baseFields, }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/channel-webhook-model.ts b/packages/domain/src/models/channel-webhook-model.ts index 02d599436..4610f800f 100644 --- a/packages/domain/src/models/channel-webhook-model.ts +++ b/packages/domain/src/models/channel-webhook-model.ts @@ -1,23 +1,23 @@ import { ChannelId, ChannelWebhookId, OrganizationId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { baseFields, JsonDate } from "./utils" -export class Model extends M.Class("ChannelWebhook")({ +class Model extends M.Class("ChannelWebhook")({ id: M.Generated(ChannelWebhookId), channelId: ChannelId, organizationId: OrganizationId, botUserId: UserId, - name: Schema.String, - description: Schema.NullOr(Schema.String), - avatarUrl: Schema.NullOr(Schema.String), - tokenHash: M.Sensitive(Schema.String), - tokenSuffix: Schema.String, - isEnabled: Schema.Boolean, + name: S.String, + description: S.NullOr(S.String), + avatarUrl: S.NullOr(S.String), + tokenHash: M.Sensitive(S.String), + tokenSuffix: S.String, + isEnabled: S.Boolean, createdBy: UserId, - lastUsedAt: Schema.NullOr(JsonDate), + lastUsedAt: S.NullOr(JsonDate), ...baseFields, }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Row, Insert, Update, Schema, Create, Patch } = M.exposeWithRow(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/chat-sync-channel-link-model.ts b/packages/domain/src/models/chat-sync-channel-link-model.ts index 87b52d728..c56f4105d 100644 --- a/packages/domain/src/models/chat-sync-channel-link-model.ts +++ b/packages/domain/src/models/chat-sync-channel-link-model.ts @@ -1,63 +1,63 @@ import { ChannelId, ExternalChannelId, SyncChannelLinkId, SyncConnectionId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const ChatSyncDirection = Schema.Literals(["both", "hazel_to_external", "external_to_hazel"]) -export type ChatSyncDirection = Schema.Schema.Type +export const ChatSyncDirection = S.Literals(["both", "hazel_to_external", "external_to_hazel"]) +export type ChatSyncDirection = S.Schema.Type -export const ChatSyncOutboundIdentityStrategy = Schema.Literals(["webhook", "fallback_bot"]) -export type ChatSyncOutboundIdentityStrategy = Schema.Schema.Type +export const ChatSyncOutboundIdentityStrategy = S.Literals(["webhook", "fallback_bot"]) +export type ChatSyncOutboundIdentityStrategy = S.Schema.Type -export const DiscordWebhookOutboundIdentityConfig = Schema.Struct({ - kind: Schema.Literal("discord.webhook"), - webhookId: Schema.NonEmptyString, - webhookToken: Schema.NonEmptyString, - defaultAvatarUrl: Schema.optional(Schema.NonEmptyString), +export const DiscordWebhookOutboundIdentityConfig = S.Struct({ + kind: S.Literal("discord.webhook"), + webhookId: S.NonEmptyString, + webhookToken: S.NonEmptyString, + defaultAvatarUrl: S.optional(S.NonEmptyString), }) -export type DiscordWebhookOutboundIdentityConfig = Schema.Schema.Type< +export type DiscordWebhookOutboundIdentityConfig = S.Schema.Type< typeof DiscordWebhookOutboundIdentityConfig > -export const SlackWebhookOutboundIdentityConfig = Schema.Struct({ - kind: Schema.Literal("slack.webhook"), - webhookUrl: Schema.NonEmptyString, - defaultIconUrl: Schema.optional(Schema.NonEmptyString), +export const SlackWebhookOutboundIdentityConfig = S.Struct({ + kind: S.Literal("slack.webhook"), + webhookUrl: S.NonEmptyString, + defaultIconUrl: S.optional(S.NonEmptyString), }) -export type SlackWebhookOutboundIdentityConfig = Schema.Schema.Type +export type SlackWebhookOutboundIdentityConfig = S.Schema.Type -export const ProviderOutboundConfig = Schema.Union([ +export const ProviderOutboundConfig = S.Union([ DiscordWebhookOutboundIdentityConfig, SlackWebhookOutboundIdentityConfig, - Schema.Struct({ - kind: Schema.NonEmptyString, + S.Struct({ + kind: S.NonEmptyString, }), ]) -export type ProviderOutboundConfig = Schema.Schema.Type +export type ProviderOutboundConfig = S.Schema.Type -export const OutboundIdentityProviders = Schema.Record(Schema.String, ProviderOutboundConfig) +export const OutboundIdentityProviders = S.Record(S.String, ProviderOutboundConfig) -export const OutboundIdentitySettings = Schema.Struct({ - enabled: Schema.Boolean, +export const OutboundIdentitySettings = S.Struct({ + enabled: S.Boolean, strategy: ChatSyncOutboundIdentityStrategy, providers: OutboundIdentityProviders, }) -export type OutboundIdentitySettings = Schema.Schema.Type +export type OutboundIdentitySettings = S.Schema.Type -export class Model extends M.Class("ChatSyncChannelLink")({ +class Model extends M.Class("ChatSyncChannelLink")({ id: M.Generated(SyncChannelLinkId), syncConnectionId: SyncConnectionId, hazelChannelId: ChannelId, externalChannelId: ExternalChannelId, - externalChannelName: Schema.NullOr(Schema.String), + externalChannelName: S.NullOr(S.String), direction: ChatSyncDirection, - isActive: Schema.Boolean, - settings: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), - lastSyncedAt: Schema.NullOr(JsonDate), + isActive: S.Boolean, + settings: S.NullOr(S.Record(S.String, S.Unknown)), + lastSyncedAt: S.NullOr(JsonDate), createdAt: M.Generated(JsonDate), - updatedAt: M.Generated(Schema.NullOr(JsonDate)), - deletedAt: M.GeneratedByApp(Schema.NullOr(JsonDate)), + updatedAt: M.Generated(S.NullOr(JsonDate)), + deletedAt: M.GeneratedByApp(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/chat-sync-connection-model.ts b/packages/domain/src/models/chat-sync-connection-model.ts index dcd39c484..d3ea40a86 100644 --- a/packages/domain/src/models/chat-sync-connection-model.ts +++ b/packages/domain/src/models/chat-sync-connection-model.ts @@ -1,31 +1,31 @@ import { IntegrationConnectionId, OrganizationId, SyncConnectionId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const ChatSyncProvider = Schema.NonEmptyString -export type ChatSyncProvider = Schema.Schema.Type +export const ChatSyncProvider = S.NonEmptyString +export type ChatSyncProvider = S.Schema.Type -export const ChatSyncConnectionStatus = Schema.Literals(["active", "paused", "error", "disabled"]) -export type ChatSyncConnectionStatus = Schema.Schema.Type +export const ChatSyncConnectionStatus = S.Literals(["active", "paused", "error", "disabled"]) +export type ChatSyncConnectionStatus = S.Schema.Type -export class Model extends M.Class("ChatSyncConnection")({ +class Model extends M.Class("ChatSyncConnection")({ id: M.Generated(SyncConnectionId), organizationId: OrganizationId, - integrationConnectionId: Schema.NullOr(IntegrationConnectionId), + integrationConnectionId: S.NullOr(IntegrationConnectionId), provider: ChatSyncProvider, - externalWorkspaceId: Schema.String, - externalWorkspaceName: Schema.NullOr(Schema.String), + externalWorkspaceId: S.String, + externalWorkspaceName: S.NullOr(S.String), status: ChatSyncConnectionStatus, - settings: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), - metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), - errorMessage: Schema.NullOr(Schema.String), - lastSyncedAt: Schema.NullOr(JsonDate), + settings: S.NullOr(S.Record(S.String, S.Unknown)), + metadata: S.NullOr(S.Record(S.String, S.Unknown)), + errorMessage: S.NullOr(S.String), + lastSyncedAt: S.NullOr(JsonDate), createdBy: UserId, createdAt: M.Generated(JsonDate), - updatedAt: M.Generated(Schema.NullOr(JsonDate)), - deletedAt: M.GeneratedByApp(Schema.NullOr(JsonDate)), + updatedAt: M.Generated(S.NullOr(JsonDate)), + deletedAt: M.GeneratedByApp(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/chat-sync-event-receipt-model.ts b/packages/domain/src/models/chat-sync-event-receipt-model.ts index 362e7b789..833a23c4f 100644 --- a/packages/domain/src/models/chat-sync-event-receipt-model.ts +++ b/packages/domain/src/models/chat-sync-event-receipt-model.ts @@ -1,27 +1,27 @@ import { SyncChannelLinkId, SyncConnectionId, SyncEventReceiptId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const ChatSyncReceiptSource = Schema.Literals(["hazel", "external"]) -export type ChatSyncReceiptSource = Schema.Schema.Type +export const ChatSyncReceiptSource = S.Literals(["hazel", "external"]) +export type ChatSyncReceiptSource = S.Schema.Type -export const ChatSyncReceiptStatus = Schema.Literals(["processed", "ignored", "failed"]) -export type ChatSyncReceiptStatus = Schema.Schema.Type +export const ChatSyncReceiptStatus = S.Literals(["processed", "ignored", "failed"]) +export type ChatSyncReceiptStatus = S.Schema.Type -export class Model extends M.Class("ChatSyncEventReceipt")({ +class Model extends M.Class("ChatSyncEventReceipt")({ id: M.Generated(SyncEventReceiptId), syncConnectionId: SyncConnectionId, - channelLinkId: Schema.NullOr(SyncChannelLinkId), + channelLinkId: S.NullOr(SyncChannelLinkId), source: ChatSyncReceiptSource, - externalEventId: Schema.NullOr(Schema.String), - dedupeKey: Schema.String, - payloadHash: Schema.NullOr(Schema.String), + externalEventId: S.NullOr(S.String), + dedupeKey: S.String, + payloadHash: S.NullOr(S.String), status: ChatSyncReceiptStatus, - errorMessage: Schema.NullOr(Schema.String), + errorMessage: S.NullOr(S.String), processedAt: M.Generated(JsonDate), createdAt: M.Generated(JsonDate), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/chat-sync-message-link-model.ts b/packages/domain/src/models/chat-sync-message-link-model.ts index b48df98ab..4ed44b479 100644 --- a/packages/domain/src/models/chat-sync-message-link-model.ts +++ b/packages/domain/src/models/chat-sync-message-link-model.ts @@ -6,26 +6,26 @@ import { SyncChannelLinkId, SyncMessageLinkId, } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import { ChatSyncReceiptSource } from "./chat-sync-event-receipt-model" import * as M from "./utils" import { JsonDate } from "./utils" -export class Model extends M.Class("ChatSyncMessageLink")({ +class Model extends M.Class("ChatSyncMessageLink")({ id: M.Generated(SyncMessageLinkId), channelLinkId: SyncChannelLinkId, hazelMessageId: MessageId, externalMessageId: ExternalMessageId, source: ChatSyncReceiptSource, - rootHazelMessageId: Schema.NullOr(MessageId), - rootExternalMessageId: Schema.NullOr(ExternalMessageId), - hazelThreadChannelId: Schema.NullOr(ChannelId), - externalThreadId: Schema.NullOr(ExternalThreadId), + rootHazelMessageId: S.NullOr(MessageId), + rootExternalMessageId: S.NullOr(ExternalMessageId), + hazelThreadChannelId: S.NullOr(ChannelId), + externalThreadId: S.NullOr(ExternalThreadId), lastSyncedAt: M.Generated(JsonDate), createdAt: M.Generated(JsonDate), - updatedAt: M.Generated(Schema.NullOr(JsonDate)), - deletedAt: M.GeneratedByApp(Schema.NullOr(JsonDate)), + updatedAt: M.Generated(S.NullOr(JsonDate)), + deletedAt: M.GeneratedByApp(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/connect-conversation-channel-model.ts b/packages/domain/src/models/connect-conversation-channel-model.ts index b9aede904..5d345cf16 100644 --- a/packages/domain/src/models/connect-conversation-channel-model.ts +++ b/packages/domain/src/models/connect-conversation-channel-model.ts @@ -1,23 +1,23 @@ import { ChannelId, ConnectConversationChannelId, ConnectConversationId, OrganizationId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const ConnectConversationChannelRole = Schema.Literals(["host", "guest"]) -export type ConnectConversationChannelRole = Schema.Schema.Type +export const ConnectConversationChannelRole = S.Literals(["host", "guest"]) +export type ConnectConversationChannelRole = S.Schema.Type -export class Model extends M.Class("ConnectConversationChannel")({ +class Model extends M.Class("ConnectConversationChannel")({ id: M.Generated(ConnectConversationChannelId), conversationId: ConnectConversationId, organizationId: OrganizationId, channelId: ChannelId, role: ConnectConversationChannelRole, - allowGuestMemberAdds: Schema.Boolean, - isActive: Schema.Boolean, + allowGuestMemberAdds: S.Boolean, + isActive: S.Boolean, createdAt: M.Generated(JsonDate), - updatedAt: M.Generated(Schema.NullOr(JsonDate)), - deletedAt: M.GeneratedByApp(Schema.NullOr(JsonDate)), + updatedAt: M.Generated(S.NullOr(JsonDate)), + deletedAt: M.GeneratedByApp(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/connect-conversation-model.ts b/packages/domain/src/models/connect-conversation-model.ts index 034f6a58f..d2e9e7eb8 100644 --- a/packages/domain/src/models/connect-conversation-model.ts +++ b/packages/domain/src/models/connect-conversation-model.ts @@ -1,22 +1,22 @@ import { ChannelId, ConnectConversationId, OrganizationId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const ConnectConversationStatus = Schema.Literals(["active", "disconnected"]) -export type ConnectConversationStatus = Schema.Schema.Type +export const ConnectConversationStatus = S.Literals(["active", "disconnected"]) +export type ConnectConversationStatus = S.Schema.Type -export class Model extends M.Class("ConnectConversation")({ +class Model extends M.Class("ConnectConversation")({ id: M.Generated(ConnectConversationId), hostOrganizationId: OrganizationId, hostChannelId: ChannelId, status: ConnectConversationStatus, - settings: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + settings: S.NullOr(S.Record(S.String, S.Unknown)), createdBy: UserId, createdAt: M.Generated(JsonDate), - updatedAt: M.Generated(Schema.NullOr(JsonDate)), - deletedAt: M.GeneratedByApp(Schema.NullOr(JsonDate)), + updatedAt: M.Generated(S.NullOr(JsonDate)), + deletedAt: M.GeneratedByApp(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/connect-invite-model.ts b/packages/domain/src/models/connect-invite-model.ts index 671096e4f..e80402002 100644 --- a/packages/domain/src/models/connect-invite-model.ts +++ b/packages/domain/src/models/connect-invite-model.ts @@ -1,32 +1,32 @@ import { ChannelId, ConnectConversationId, ConnectInviteId, OrganizationId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const ConnectInviteStatus = Schema.Literals(["pending", "accepted", "declined", "revoked", "expired"]) -export type ConnectInviteStatus = Schema.Schema.Type +export const ConnectInviteStatus = S.Literals(["pending", "accepted", "declined", "revoked", "expired"]) +export type ConnectInviteStatus = S.Schema.Type -export const ConnectInviteTargetKind = Schema.Literals(["slug", "email"]) -export type ConnectInviteTargetKind = Schema.Schema.Type +export const ConnectInviteTargetKind = S.Literals(["slug", "email"]) +export type ConnectInviteTargetKind = S.Schema.Type -export class Model extends M.Class("ConnectInvite")({ +class Model extends M.Class("ConnectInvite")({ id: M.Generated(ConnectInviteId), conversationId: ConnectConversationId, hostOrganizationId: OrganizationId, hostChannelId: ChannelId, targetKind: ConnectInviteTargetKind, - targetValue: Schema.String, - guestOrganizationId: Schema.NullOr(OrganizationId), + targetValue: S.String, + guestOrganizationId: S.NullOr(OrganizationId), status: ConnectInviteStatus, - allowGuestMemberAdds: Schema.Boolean, + allowGuestMemberAdds: S.Boolean, invitedBy: UserId, - acceptedBy: Schema.NullOr(UserId), - acceptedAt: Schema.NullOr(JsonDate), - expiresAt: Schema.NullOr(JsonDate), + acceptedBy: S.NullOr(UserId), + acceptedAt: S.NullOr(JsonDate), + expiresAt: S.NullOr(JsonDate), createdAt: M.Generated(JsonDate), - updatedAt: M.Generated(Schema.NullOr(JsonDate)), - deletedAt: M.GeneratedByApp(Schema.NullOr(JsonDate)), + updatedAt: M.Generated(S.NullOr(JsonDate)), + deletedAt: M.GeneratedByApp(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/connect-participant-model.ts b/packages/domain/src/models/connect-participant-model.ts index 80e1cbeec..8a744ed9e 100644 --- a/packages/domain/src/models/connect-participant-model.ts +++ b/packages/domain/src/models/connect-participant-model.ts @@ -1,20 +1,20 @@ import { ChannelId, ConnectConversationId, ConnectParticipantId, OrganizationId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export class Model extends M.Class("ConnectParticipant")({ +class Model extends M.Class("ConnectParticipant")({ id: M.Generated(ConnectParticipantId), conversationId: ConnectConversationId, channelId: ChannelId, userId: UserId, homeOrganizationId: OrganizationId, - isExternal: Schema.Boolean, - addedBy: Schema.NullOr(UserId), + isExternal: S.Boolean, + addedBy: S.NullOr(UserId), createdAt: M.Generated(JsonDate), - updatedAt: M.Generated(Schema.NullOr(JsonDate)), - deletedAt: M.GeneratedByApp(Schema.NullOr(JsonDate)), + updatedAt: M.Generated(S.NullOr(JsonDate)), + deletedAt: M.GeneratedByApp(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/custom-emoji-model.ts b/packages/domain/src/models/custom-emoji-model.ts index 182e5e1bc..b182f245c 100644 --- a/packages/domain/src/models/custom-emoji-model.ts +++ b/packages/domain/src/models/custom-emoji-model.ts @@ -1,18 +1,18 @@ import { CustomEmojiId, OrganizationId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export class Model extends M.Class("CustomEmoji")({ +class Model extends M.Class("CustomEmoji")({ id: M.Generated(CustomEmojiId), organizationId: OrganizationId, - name: Schema.String, - imageUrl: Schema.String, + name: S.String, + imageUrl: S.String, createdBy: M.GeneratedByApp(UserId), createdAt: M.Generated(JsonDate), - updatedAt: M.Generated(Schema.NullOr(JsonDate)), - deletedAt: M.Generated(Schema.NullOr(JsonDate)), + updatedAt: M.Generated(S.NullOr(JsonDate)), + deletedAt: M.Generated(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/github-subscription-model.ts b/packages/domain/src/models/github-subscription-model.ts index 375e56524..ecd5d8022 100644 --- a/packages/domain/src/models/github-subscription-model.ts +++ b/packages/domain/src/models/github-subscription-model.ts @@ -1,33 +1,33 @@ import { GitHubEventType, GitHubEventTypes } from "@hazel/integrations/github/schema" import { ChannelId, GitHubSubscriptionId, OrganizationId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" // Re-export from integrations for backwards compatibility export { GitHubEventType, GitHubEventTypes } -export class Model extends M.Class("GitHubSubscription")({ +class Model extends M.Class("GitHubSubscription")({ id: M.Generated(GitHubSubscriptionId), channelId: ChannelId, organizationId: OrganizationId, // Repository identification - GitHub's numeric ID is stable across renames - repositoryId: Schema.Number, - repositoryFullName: Schema.String, // "owner/repo" for display - repositoryOwner: Schema.String, - repositoryName: Schema.String, + repositoryId: S.Number, + repositoryFullName: S.String, // "owner/repo" for display + repositoryOwner: S.String, + repositoryName: S.String, // Event type filters enabledEvents: GitHubEventTypes, // Optional branch filter for push events (null = all branches) - branchFilter: Schema.NullOr(Schema.String), + branchFilter: S.NullOr(S.String), // Whether the subscription is active - isEnabled: Schema.Boolean, + isEnabled: S.Boolean, // Audit fields createdBy: UserId, createdAt: M.Generated(JsonDate), - updatedAt: M.Generated(Schema.NullOr(JsonDate)), - deletedAt: M.GeneratedByApp(Schema.NullOr(JsonDate)), + updatedAt: M.Generated(S.NullOr(JsonDate)), + deletedAt: M.GeneratedByApp(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/integration-connection-model.ts b/packages/domain/src/models/integration-connection-model.ts index b44a67842..edb354dec 100644 --- a/packages/domain/src/models/integration-connection-model.ts +++ b/packages/domain/src/models/integration-connection-model.ts @@ -1,9 +1,9 @@ import { IntegrationConnectionId, OrganizationId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const IntegrationProvider = Schema.Literals([ +export const IntegrationProvider = S.Literals([ "linear", "github", "figma", @@ -11,32 +11,32 @@ export const IntegrationProvider = Schema.Literals([ "discord", "craft", ]) -export type IntegrationProvider = Schema.Schema.Type +export type IntegrationProvider = S.Schema.Type -export const ConnectionLevel = Schema.Literals(["organization", "user"]) -export type ConnectionLevel = Schema.Schema.Type +export const ConnectionLevel = S.Literals(["organization", "user"]) +export type ConnectionLevel = S.Schema.Type -export const ConnectionStatus = Schema.Literals(["active", "expired", "revoked", "error", "suspended"]) -export type ConnectionStatus = Schema.Schema.Type +export const ConnectionStatus = S.Literals(["active", "expired", "revoked", "error", "suspended"]) +export type ConnectionStatus = S.Schema.Type -export class Model extends M.Class("IntegrationConnection")({ +class Model extends M.Class("IntegrationConnection")({ id: M.Generated(IntegrationConnectionId), provider: IntegrationProvider, organizationId: OrganizationId, - userId: Schema.NullOr(UserId), + userId: S.NullOr(UserId), level: ConnectionLevel, status: ConnectionStatus, - externalAccountId: Schema.NullOr(Schema.String), - externalAccountName: Schema.NullOr(Schema.String), + externalAccountId: S.NullOr(S.String), + externalAccountName: S.NullOr(S.String), connectedBy: UserId, - settings: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), - metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), - errorMessage: Schema.NullOr(Schema.String), - lastUsedAt: Schema.NullOr(JsonDate), + settings: S.NullOr(S.Record(S.String, S.Unknown)), + metadata: S.NullOr(S.Record(S.String, S.Unknown)), + errorMessage: S.NullOr(S.String), + lastUsedAt: S.NullOr(JsonDate), createdAt: M.Generated(JsonDate), - updatedAt: M.Generated(Schema.NullOr(JsonDate)), - deletedAt: M.GeneratedByApp(Schema.NullOr(JsonDate)), + updatedAt: M.Generated(S.NullOr(JsonDate)), + deletedAt: M.GeneratedByApp(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/integration-request-model.ts b/packages/domain/src/models/integration-request-model.ts index 79194c4c8..490885335 100644 --- a/packages/domain/src/models/integration-request-model.ts +++ b/packages/domain/src/models/integration-request-model.ts @@ -1,22 +1,22 @@ import { IntegrationRequestId, OrganizationId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const IntegrationRequestStatus = Schema.Literals(["pending", "reviewed", "planned", "rejected"]) -export type IntegrationRequestStatus = Schema.Schema.Type +export const IntegrationRequestStatus = S.Literals(["pending", "reviewed", "planned", "rejected"]) +export type IntegrationRequestStatus = S.Schema.Type -export class Model extends M.Class("IntegrationRequest")({ +class Model extends M.Class("IntegrationRequest")({ id: M.Generated(IntegrationRequestId), organizationId: OrganizationId, requestedBy: UserId, - integrationName: Schema.NonEmptyString, - integrationUrl: Schema.NullOr(Schema.String), - description: Schema.NullOr(Schema.String), + integrationName: S.NonEmptyString, + integrationUrl: S.NullOr(S.String), + description: S.NullOr(S.String), status: IntegrationRequestStatus, createdAt: M.Generated(JsonDate), updatedAt: M.Generated(JsonDate), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/integration-token-model.ts b/packages/domain/src/models/integration-token-model.ts index 941d0dd5f..58318d120 100644 --- a/packages/domain/src/models/integration-token-model.ts +++ b/packages/domain/src/models/integration-token-model.ts @@ -1,24 +1,24 @@ import { IntegrationConnectionId, IntegrationTokenId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export class Model extends M.Class("IntegrationToken")({ +class Model extends M.Class("IntegrationToken")({ id: M.Generated(IntegrationTokenId), connectionId: IntegrationConnectionId, - encryptedAccessToken: Schema.String, - encryptedRefreshToken: Schema.NullOr(Schema.String), - iv: Schema.String, - refreshTokenIv: Schema.NullOr(Schema.String), - encryptionKeyVersion: Schema.Number, - tokenType: Schema.NullOr(Schema.String), - scope: Schema.NullOr(Schema.String), - expiresAt: Schema.NullOr(JsonDate), - refreshTokenExpiresAt: Schema.NullOr(JsonDate), - lastRefreshedAt: Schema.NullOr(JsonDate), + encryptedAccessToken: S.String, + encryptedRefreshToken: S.NullOr(S.String), + iv: S.String, + refreshTokenIv: S.NullOr(S.String), + encryptionKeyVersion: S.Number, + tokenType: S.NullOr(S.String), + scope: S.NullOr(S.String), + expiresAt: S.NullOr(JsonDate), + refreshTokenExpiresAt: S.NullOr(JsonDate), + lastRefreshedAt: S.NullOr(JsonDate), createdAt: M.Generated(JsonDate), updatedAt: M.Generated(JsonDate), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/invitation-model.ts b/packages/domain/src/models/invitation-model.ts index d6111a5c8..906ca6af0 100644 --- a/packages/domain/src/models/invitation-model.ts +++ b/packages/domain/src/models/invitation-model.ts @@ -1,24 +1,24 @@ import { InvitationId, OrganizationId, UserId, WorkOSInvitationId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const InvitationStatus = Schema.Literals(["pending", "accepted", "expired", "revoked"]) -export type InvitationStatus = Schema.Schema.Type +export const InvitationStatus = S.Literals(["pending", "accepted", "expired", "revoked"]) +export type InvitationStatus = S.Schema.Type -export class Model extends M.Class("Invitation")({ +class Model extends M.Class("Invitation")({ id: M.Generated(InvitationId), - invitationUrl: Schema.String, + invitationUrl: S.String, workosInvitationId: WorkOSInvitationId, organizationId: OrganizationId, - email: Schema.String, - invitedBy: Schema.NullOr(UserId), + email: S.String, + invitedBy: S.NullOr(UserId), invitedAt: JsonDate, expiresAt: JsonDate, status: InvitationStatus, - acceptedAt: Schema.NullOr(JsonDate), - acceptedBy: Schema.NullOr(UserId), + acceptedAt: S.NullOr(JsonDate), + acceptedBy: S.NullOr(UserId), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/message-integration-link-model.ts b/packages/domain/src/models/message-integration-link-model.ts index 3f0ea7c26..98a962021 100644 --- a/packages/domain/src/models/message-integration-link-model.ts +++ b/packages/domain/src/models/message-integration-link-model.ts @@ -1,25 +1,25 @@ import { IntegrationConnectionId, MessageId, MessageIntegrationLinkId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import { IntegrationProvider } from "./integration-connection-model" import * as M from "./utils" import { JsonDate } from "./utils" -export const LinkType = Schema.Literals(["created", "mentioned", "resolved", "linked"]) -export type LinkType = Schema.Schema.Type +export const LinkType = S.Literals(["created", "mentioned", "resolved", "linked"]) +export type LinkType = S.Schema.Type -export class Model extends M.Class("MessageIntegrationLink")({ +class Model extends M.Class("MessageIntegrationLink")({ id: M.Generated(MessageIntegrationLinkId), messageId: MessageId, connectionId: IntegrationConnectionId, provider: IntegrationProvider, - externalId: Schema.String, - externalUrl: Schema.String, - externalTitle: Schema.NullOr(Schema.String), + externalId: S.String, + externalUrl: S.String, + externalTitle: S.NullOr(S.String), linkType: LinkType, - metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + metadata: S.NullOr(S.Record(S.String, S.Unknown)), createdAt: M.Generated(JsonDate), updatedAt: M.Generated(JsonDate), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/message-model.ts b/packages/domain/src/models/message-model.ts index b30807006..3fd7a0687 100644 --- a/packages/domain/src/models/message-model.ts +++ b/packages/domain/src/models/message-model.ts @@ -1,36 +1,45 @@ import { AttachmentId, ChannelId, ConnectConversationId, MessageId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import { MessageEmbeds } from "./message-embed-schema" import * as M from "./utils" import { baseFields } from "./utils" -export class Model extends M.Class("Message")({ +class Model extends M.Class("Message")({ id: M.Generated(MessageId), channelId: ChannelId, - conversationId: M.GeneratedOptional(Schema.NullOr(ConnectConversationId)), + conversationId: M.GeneratedOptional(S.NullOr(ConnectConversationId)), authorId: M.GeneratedByApp(UserId), - content: Schema.String, - embeds: Schema.NullOr(MessageEmbeds), - replyToMessageId: Schema.NullOr(MessageId), - threadChannelId: Schema.NullOr(ChannelId), + content: S.String, + embeds: S.NullOr(MessageEmbeds), + replyToMessageId: S.NullOr(MessageId), + threadChannelId: S.NullOr(ChannelId), ...baseFields, }) {} // Custom insert schema that includes attachmentIds for linking -export const Insert = Schema.Struct({ - ...Model.insert.fields, - conversationId: Schema.optional(Schema.NullOr(ConnectConversationId)), - attachmentIds: Schema.optional(Schema.Array(AttachmentId)), +export const Insert = S.Struct({ + ...M.structFields(Model.insert), + conversationId: S.optional(S.NullOr(ConnectConversationId)), + attachmentIds: S.optional(S.Array(AttachmentId)), }) -export const Update = Model.update +export const { Update, Schema } = M.expose(Model) /** * Custom update schema for JSON API - only allows mutable fields. * Excludes immutable relationship fields (channelId, replyToMessageId, threadChannelId) * to prevent users from moving messages between channels or fabricating conversation context. */ -export const JsonUpdate = Schema.Struct({ - content: Schema.optionalKey((Model.jsonUpdate as any).fields.content), - embeds: Schema.optionalKey((Model.jsonUpdate as any).fields.embeds), +const JsonUpdate = S.Struct({ + content: S.optionalKey(M.structFields(Model.jsonUpdate).content), + embeds: S.optionalKey(M.structFields(Model.jsonUpdate).embeds), }) + +export type Type = typeof Schema.Type + +export const Create = S.Struct({ + ...M.structFields(Model.jsonCreate), + attachmentIds: S.optional(S.Array(AttachmentId)), +}) + +export const Patch = JsonUpdate diff --git a/packages/domain/src/models/message-reaction-model.ts b/packages/domain/src/models/message-reaction-model.ts index 313168989..c441f8c96 100644 --- a/packages/domain/src/models/message-reaction-model.ts +++ b/packages/domain/src/models/message-reaction-model.ts @@ -1,20 +1,21 @@ import { ChannelId, ConnectConversationId, MessageId, MessageReactionId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export class Model extends M.Class("MessageReaction")({ +class Model extends M.Class("MessageReaction")({ id: M.Generated(MessageReactionId), messageId: MessageId, channelId: ChannelId, - conversationId: M.GeneratedOptional(Schema.NullOr(ConnectConversationId)), + conversationId: M.GeneratedOptional(S.NullOr(ConnectConversationId)), userId: M.GeneratedByApp(UserId), - emoji: Schema.String, + emoji: S.String, createdAt: M.Generated(JsonDate), }) {} -export const Insert = Schema.Struct({ - ...Model.insert.fields, - conversationId: Schema.optional(Schema.NullOr(ConnectConversationId)), +export const Insert = S.Struct({ + ...M.structFields(Model.insert), + conversationId: S.optional(S.NullOr(ConnectConversationId)), }) -export const Update = Model.update +export const { Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/notification-model.ts b/packages/domain/src/models/notification-model.ts index 5d1be4bc4..fed35e45e 100644 --- a/packages/domain/src/models/notification-model.ts +++ b/packages/domain/src/models/notification-model.ts @@ -1,18 +1,18 @@ import { NotificationId, OrganizationMemberId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export class Model extends M.Class("Notification")({ +class Model extends M.Class("Notification")({ id: M.Generated(NotificationId), memberId: OrganizationMemberId, - targetedResourceId: Schema.NullOr(Schema.String.check(Schema.isUUID())), - targetedResourceType: Schema.NullOr(Schema.String), - resourceId: Schema.NullOr(Schema.String.check(Schema.isUUID())), - resourceType: Schema.NullOr(Schema.String), + targetedResourceId: S.NullOr(S.String.check(S.isUUID())), + targetedResourceType: S.NullOr(S.String), + resourceId: S.NullOr(S.String.check(S.isUUID())), + resourceType: S.NullOr(S.String), createdAt: M.Generated(JsonDate), - readAt: Schema.NullOr(JsonDate), + readAt: S.NullOr(JsonDate), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/organization-member-model.ts b/packages/domain/src/models/organization-member-model.ts index f84905594..3110bf277 100644 --- a/packages/domain/src/models/organization-member-model.ts +++ b/packages/domain/src/models/organization-member-model.ts @@ -1,22 +1,22 @@ import { OrganizationId, OrganizationMemberId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { baseFields, JsonDate } from "./utils" -export const OrganizationRole = Schema.Literals(["admin", "member", "owner"]) -export type OrganizationRole = Schema.Schema.Type +export const OrganizationRole = S.Literals(["admin", "member", "owner"]) +export type OrganizationRole = S.Schema.Type -export class Model extends M.Class("OrganizationMember")({ +class Model extends M.Class("OrganizationMember")({ id: M.Generated(OrganizationMemberId), organizationId: OrganizationId, userId: M.GeneratedByApp(UserId), role: OrganizationRole, - nickname: Schema.NullishOr(Schema.String), + nickname: S.NullishOr(S.String), joinedAt: JsonDate, - invitedBy: Schema.NullOr(UserId), - deletedAt: Schema.NullOr(JsonDate), + invitedBy: S.NullOr(UserId), + deletedAt: S.NullOr(JsonDate), createdAt: M.Generated(JsonDate), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/organization-model.ts b/packages/domain/src/models/organization-model.ts index 18e3a6c9a..4db1b7a30 100644 --- a/packages/domain/src/models/organization-model.ts +++ b/packages/domain/src/models/organization-model.ts @@ -1,17 +1,17 @@ import { OrganizationId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { baseFields } from "./utils" -export class Model extends M.Class("Organization")({ +class Model extends M.Class("Organization")({ id: M.Generated(OrganizationId), - name: Schema.String, - slug: Schema.NullOr(Schema.String), - logoUrl: Schema.NullOr(Schema.String), - settings: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), - isPublic: Schema.Boolean, + name: S.String, + slug: S.NullOr(S.String), + logoUrl: S.NullOr(S.String), + settings: S.NullOr(S.Record(S.String, S.Unknown)), + isPublic: S.Boolean, ...baseFields, }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/pinned-message-model.ts b/packages/domain/src/models/pinned-message-model.ts index ea6970ddd..5b855c02b 100644 --- a/packages/domain/src/models/pinned-message-model.ts +++ b/packages/domain/src/models/pinned-message-model.ts @@ -2,7 +2,7 @@ import { ChannelId, MessageId, PinnedMessageId, UserId } from "@hazel/schema" import * as M from "./utils" import { JsonDate } from "./utils" -export class Model extends M.Class("PinnedMessage")({ +class Model extends M.Class("PinnedMessage")({ id: M.Generated(PinnedMessageId), channelId: ChannelId, messageId: MessageId, @@ -10,5 +10,5 @@ export class Model extends M.Class("PinnedMessage")({ pinnedAt: JsonDate, }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/rss-subscription-model.ts b/packages/domain/src/models/rss-subscription-model.ts index 150889813..a7e35dbc2 100644 --- a/packages/domain/src/models/rss-subscription-model.ts +++ b/packages/domain/src/models/rss-subscription-model.ts @@ -1,28 +1,28 @@ import { ChannelId, OrganizationId, RssSubscriptionId, UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export class Model extends M.Class("RssSubscription")({ +class Model extends M.Class("RssSubscription")({ id: M.Generated(RssSubscriptionId), channelId: ChannelId, organizationId: OrganizationId, - feedUrl: Schema.String, - feedTitle: Schema.NullOr(Schema.String), - feedIconUrl: Schema.NullOr(Schema.String), - lastFetchedAt: M.Generated(Schema.NullOr(JsonDate)), - lastItemPublishedAt: M.Generated(Schema.NullOr(JsonDate)), - lastItemGuid: M.Generated(Schema.NullOr(Schema.String)), - consecutiveErrors: M.Generated(Schema.Number), - lastErrorMessage: M.Generated(Schema.NullOr(Schema.String)), - lastErrorAt: M.Generated(Schema.NullOr(JsonDate)), - isEnabled: Schema.Boolean, - pollingIntervalMinutes: Schema.Number, + feedUrl: S.String, + feedTitle: S.NullOr(S.String), + feedIconUrl: S.NullOr(S.String), + lastFetchedAt: M.Generated(S.NullOr(JsonDate)), + lastItemPublishedAt: M.Generated(S.NullOr(JsonDate)), + lastItemGuid: M.Generated(S.NullOr(S.String)), + consecutiveErrors: M.Generated(S.Number), + lastErrorMessage: M.Generated(S.NullOr(S.String)), + lastErrorAt: M.Generated(S.NullOr(JsonDate)), + isEnabled: S.Boolean, + pollingIntervalMinutes: S.Number, createdBy: UserId, createdAt: M.Generated(JsonDate), - updatedAt: M.Generated(Schema.NullOr(JsonDate)), - deletedAt: M.GeneratedByApp(Schema.NullOr(JsonDate)), + updatedAt: M.Generated(S.NullOr(JsonDate)), + deletedAt: M.GeneratedByApp(S.NullOr(JsonDate)), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/schema-migration.test.ts b/packages/domain/src/models/schema-migration.test.ts new file mode 100644 index 000000000..b47bb5944 --- /dev/null +++ b/packages/domain/src/models/schema-migration.test.ts @@ -0,0 +1,123 @@ +import { Schema } from "effect" +import { describe, expect, it } from "vitest" +import { Channel, ChannelWebhook, Message, Organization } from "./index" + +const ORGANIZATION_ID = "00000000-0000-4000-8000-000000000001" +const CHANNEL_ID = "00000000-0000-4000-8000-000000000002" +const CHANNEL_SECTION_ID = "00000000-0000-4000-8000-000000000003" +const USER_ID = "00000000-0000-4000-8000-000000000004" +const MESSAGE_ID = "00000000-0000-4000-8000-000000000005" +const ATTACHMENT_ID = "00000000-0000-4000-8000-000000000006" +const WEBHOOK_ID = "00000000-0000-4000-8000-000000000007" +const CREATED_AT = "2026-03-17T12:00:00.000Z" +const UPDATED_AT = "2026-03-17T12:30:00.000Z" + +describe("model-derived entity schemas", () => { + it("roundtrips a normal entity public payload without changing the wire shape", () => { + const raw = { + id: ORGANIZATION_ID, + name: "Hazel", + slug: "hazel", + logoUrl: null, + settings: { theme: "light" }, + isPublic: false, + createdAt: CREATED_AT, + updatedAt: UPDATED_AT, + deletedAt: null, + } + + const decoded = Schema.decodeUnknownSync(Organization.Schema)(raw) + const encoded = Schema.encodeUnknownSync(Organization.Schema)(decoded) + + expect(encoded).toEqual(raw) + }) + + it("keeps optimistic ids on create while leaving repo-only fields in insert", () => { + const createPayload = { + id: CHANNEL_ID, + name: "general", + icon: null, + type: "public" as const, + organizationId: ORGANIZATION_ID, + parentChannelId: null, + sectionId: CHANNEL_SECTION_ID, + } + + const decodedCreate = Schema.decodeUnknownSync(Channel.Create)(createPayload) + const decodedInsert = Schema.decodeUnknownSync(Channel.Insert)({ + ...createPayload, + deletedAt: null, + }) + + expect(decodedCreate.id).toBe(CHANNEL_ID) + expect("deletedAt" in decodedCreate).toBe(false) + expect(decodedInsert.deletedAt).toBeNull() + }) + + it("keeps sensitive row-only fields out of the public webhook schema", () => { + const row = { + id: WEBHOOK_ID, + channelId: CHANNEL_ID, + organizationId: ORGANIZATION_ID, + botUserId: USER_ID, + name: "Deploy hook", + description: null, + avatarUrl: null, + tokenHash: "hashed-token", + tokenSuffix: "cafe", + isEnabled: true, + createdBy: USER_ID, + lastUsedAt: null, + createdAt: CREATED_AT, + updatedAt: UPDATED_AT, + deletedAt: null, + } + + const publicRaw = { + ...row, + tokenHash: undefined, + } + delete publicRaw.tokenHash + + expect("tokenHash" in ChannelWebhook.Row.fields).toBe(true) + expect("tokenHash" in ChannelWebhook.Schema.fields).toBe(false) + expect(Schema.decodeUnknownSync(ChannelWebhook.Row)(row).tokenHash).toBe("hashed-token") + expect( + Schema.encodeUnknownSync(ChannelWebhook.Schema)( + Schema.decodeUnknownSync(ChannelWebhook.Schema)(publicRaw), + ), + ).toEqual(publicRaw) + }) + + it("preserves the custom message create and patch shapes", () => { + const createPayload = { + channelId: CHANNEL_ID, + authorId: USER_ID, + content: "hello", + embeds: null, + replyToMessageId: null, + threadChannelId: null, + attachmentIds: [ATTACHMENT_ID], + } + + const messageRaw = { + id: MESSAGE_ID, + channelId: CHANNEL_ID, + conversationId: null, + authorId: USER_ID, + content: "hello", + embeds: null, + replyToMessageId: null, + threadChannelId: null, + createdAt: CREATED_AT, + updatedAt: UPDATED_AT, + deletedAt: null, + } + + expect(Schema.decodeUnknownSync(Message.Create)(createPayload).attachmentIds).toEqual([ATTACHMENT_ID]) + expect(Object.keys(Message.Patch.fields).sort()).toEqual(["content", "embeds"]) + expect( + Schema.encodeUnknownSync(Message.Schema)(Schema.decodeUnknownSync(Message.Schema)(messageRaw)), + ).toEqual(messageRaw) + }) +}) diff --git a/packages/domain/src/models/typing-indicator-model.ts b/packages/domain/src/models/typing-indicator-model.ts index 9db19c545..5795e22b8 100644 --- a/packages/domain/src/models/typing-indicator-model.ts +++ b/packages/domain/src/models/typing-indicator-model.ts @@ -1,16 +1,16 @@ import { ChannelId, ChannelMemberId, TypingIndicatorId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" -export class Model extends M.Class("TypingIndicator")({ +class Model extends M.Class("TypingIndicator")({ id: M.Generated(TypingIndicatorId), channelId: ChannelId, memberId: ChannelMemberId, - lastTyped: Schema.Number.annotate({ + lastTyped: S.Number.annotate({ title: "LastTyped", description: "Unix timestamp of last typing activity", }), }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/user-model.ts b/packages/domain/src/models/user-model.ts index 81c2dfb6a..197268403 100644 --- a/packages/domain/src/models/user-model.ts +++ b/packages/domain/src/models/user-model.ts @@ -1,45 +1,45 @@ import { UserId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import { UserThemeSettings } from "./theme-model" import * as M from "./utils" import { baseFields } from "./utils" -export const UserType = Schema.Literals(["user", "machine"]) -export type UserType = Schema.Schema.Type +export const UserType = S.Literals(["user", "machine"]) +export type UserType = S.Schema.Type /** * Time in HH:MM format (00:00 - 23:59) */ -export const TimeString = Schema.String.check(Schema.isPattern(/^([01]\d|2[0-3]):([0-5]\d)$/)).pipe( - Schema.brand("TimeString"), +export const TimeString = S.String.check(S.isPattern(/^([01]\d|2[0-3]):([0-5]\d)$/)).pipe( + S.brand("TimeString"), ) -export type TimeString = Schema.Schema.Type +export type TimeString = S.Schema.Type /** * Schema for user settings stored in the database */ -export const UserSettingsSchema = Schema.Struct({ - doNotDisturb: Schema.optional(Schema.Boolean), - quietHoursStart: Schema.optional(TimeString), - quietHoursEnd: Schema.optional(TimeString), - showQuietHoursInStatus: Schema.optional(Schema.Boolean), - theme: Schema.optional(UserThemeSettings), +export const UserSettingsSchema = S.Struct({ + doNotDisturb: S.optional(S.Boolean), + quietHoursStart: S.optional(TimeString), + quietHoursEnd: S.optional(TimeString), + showQuietHoursInStatus: S.optional(S.Boolean), + theme: S.optional(UserThemeSettings), }) -export type UserSettings = Schema.Schema.Type +export type UserSettings = S.Schema.Type -export class Model extends M.Class("User")({ +class Model extends M.Class("User")({ id: M.Generated(UserId), - externalId: Schema.String, - email: Schema.String, - firstName: Schema.String, - lastName: Schema.String, - avatarUrl: Schema.NullishOr(Schema.NonEmptyString), + externalId: S.String, + email: S.String, + firstName: S.String, + lastName: S.String, + avatarUrl: S.NullishOr(S.NonEmptyString), userType: UserType, - settings: Schema.NullOr(UserSettingsSchema), - isOnboarded: Schema.Boolean, - timezone: Schema.NullOr(Schema.String), + settings: S.NullOr(UserSettingsSchema), + isOnboarded: S.Boolean, + timezone: S.NullOr(S.String), ...baseFields, }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/user-presence-status-model.ts b/packages/domain/src/models/user-presence-status-model.ts index c668f7e70..5d9cab7f3 100644 --- a/packages/domain/src/models/user-presence-status-model.ts +++ b/packages/domain/src/models/user-presence-status-model.ts @@ -1,23 +1,23 @@ import { ChannelId, UserId, UserPresenceStatusId } from "@hazel/schema" -import { Schema } from "effect" +import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const UserPresenceStatusEnum = Schema.Literals(["online", "away", "busy", "dnd", "offline"]) -export type UserPresenceStatusEnum = Schema.Schema.Type +export const UserPresenceStatusEnum = S.Literals(["online", "away", "busy", "dnd", "offline"]) +export type UserPresenceStatusEnum = S.Schema.Type -export class Model extends M.Class("UserPresenceStatus")({ +class Model extends M.Class("UserPresenceStatus")({ id: M.Generated(UserPresenceStatusId), userId: UserId, status: UserPresenceStatusEnum, - customMessage: Schema.NullOr(Schema.String), - statusEmoji: Schema.NullOr(Schema.String), - statusExpiresAt: Schema.NullOr(JsonDate), - activeChannelId: Schema.NullOr(ChannelId), - suppressNotifications: Schema.Boolean, + customMessage: S.NullOr(S.String), + statusEmoji: S.NullOr(S.String), + statusExpiresAt: S.NullOr(JsonDate), + activeChannelId: S.NullOr(ChannelId), + suppressNotifications: S.Boolean, updatedAt: JsonDate, lastSeenAt: JsonDate, }) {} -export const Insert = Model.insert -export const Update = Model.update +export const { Insert, Update, Schema, Create, Patch } = M.expose(Model) +export type Type = typeof Schema.Type diff --git a/packages/domain/src/models/utils.ts b/packages/domain/src/models/utils.ts index 5ffd92234..aad07c1d6 100644 --- a/packages/domain/src/models/utils.ts +++ b/packages/domain/src/models/utils.ts @@ -21,15 +21,6 @@ export type Any = Schema.Top & { readonly jsonUpdate: Schema.Top } -export type AnyNoContext = Schema.Top & { - readonly fields: Schema.Struct.Fields - readonly insert: Schema.Top - readonly update: Schema.Top - readonly json: Schema.Top - readonly jsonCreate: Schema.Top - readonly jsonUpdate: Schema.Top -} - export type VariantsDatabase = "select" | "insert" | "update" export type VariantsJson = "json" | "jsonCreate" | "jsonUpdate" @@ -67,6 +58,10 @@ export { export const fields:
>(self: A) => A[typeof VariantSchema.TypeId] = VariantSchema.fields +export const structFields = ( + self: A, +): A["fields"] => self.fields + export const Override: (value: A) => A & Brand<"Override"> = VariantSchema.Override export interface Generated extends VariantSchema.Field<{ @@ -339,15 +334,66 @@ export const UuidV4Insert = ( /** A boolean parsed from 0 or 1. */ export const BooleanFromNumber: typeof Schema.BooleanFromBit = Schema.BooleanFromBit -export interface EntitySchema extends Schema.Top { - readonly fields: Schema.Struct.Fields - readonly insert: Schema.Top - readonly update: Schema.Top - readonly json: Schema.Top - readonly jsonCreate: Schema.Top - readonly jsonUpdate: Schema.Top +export interface ExposedModel< + InsertSchema extends Schema.Top, + UpdateSchema extends Schema.Top, + JsonSchema extends Schema.Top, + CreateSchema extends Schema.Top, + PatchSchema extends Schema.Top, +> { + readonly Insert: InsertSchema + readonly Update: UpdateSchema + readonly Schema: JsonSchema + readonly Create: CreateSchema + readonly Patch: PatchSchema +} + +export interface ExposedModelWithRow< + RowSchema extends Any, + InsertSchema extends Schema.Top, + UpdateSchema extends Schema.Top, + JsonSchema extends Schema.Top, + CreateSchema extends Schema.Top, + PatchSchema extends Schema.Top, +> extends ExposedModel { + readonly Row: RowSchema } +export const expose = < + Model extends Any, + InsertSchema extends Schema.Top = Model["insert"], + UpdateSchema extends Schema.Top = Model["update"], + JsonSchema extends Schema.Top = Model["json"], + CreateSchema extends Schema.Top = Model["jsonCreate"], + PatchSchema extends Schema.Top = Model["jsonUpdate"], +>( + model: Model, + overrides: Partial> = {}, +): ExposedModel => ({ + Insert: overrides.Insert ?? ((model.insert as unknown) as InsertSchema), + Update: overrides.Update ?? ((model.update as unknown) as UpdateSchema), + Schema: overrides.Schema ?? ((model.json as unknown) as JsonSchema), + Create: overrides.Create ?? ((model.jsonCreate as unknown) as CreateSchema), + Patch: overrides.Patch ?? ((model.jsonUpdate as unknown) as PatchSchema), +}) + +export const exposeWithRow = < + Model extends Any, + InsertSchema extends Schema.Top = Model["insert"], + UpdateSchema extends Schema.Top = Model["update"], + JsonSchema extends Schema.Top = Model["json"], + CreateSchema extends Schema.Top = Model["jsonCreate"], + PatchSchema extends Schema.Top = Model["jsonUpdate"], +>( + model: Model, + overrides: Partial< + ExposedModelWithRow + > = {}, +): ExposedModelWithRow => ({ + ...expose(model, overrides), + Row: overrides.Row ?? model, +}) + // Helper utilities for common model fields export const JsonDate = Schema.Union([Schema.DateTimeUtcFromString, Schema.Date]).pipe( Schema.annotate({ diff --git a/packages/domain/src/rpc/attachments.ts b/packages/domain/src/rpc/attachments.ts index 2af962aaf..2d8fafe55 100644 --- a/packages/domain/src/rpc/attachments.ts +++ b/packages/domain/src/rpc/attachments.ts @@ -69,7 +69,7 @@ export class AttachmentRpcs extends RpcGroup.make( */ Rpc.make("attachment.complete", { payload: Schema.Struct({ id: AttachmentId }), - success: Attachment.Model, + success: Attachment.Schema, error: Schema.Union([AttachmentNotFoundError, UnauthorizedError, InternalServerError]), }) .annotate(RequiredScopes, ["attachments:write"]) diff --git a/packages/domain/src/rpc/bots.ts b/packages/domain/src/rpc/bots.ts index 46e81b188..12ee67bee 100644 --- a/packages/domain/src/rpc/bots.ts +++ b/packages/domain/src/rpc/bots.ts @@ -14,7 +14,7 @@ import { ApiScope } from "../scopes/api-scope" * Contains the bot data and a transaction ID for optimistic updates. */ export class BotResponse extends Schema.Class("BotResponse")({ - data: Bot.Model.json, + data: Bot.Schema, transactionId: TransactionId, }) {} @@ -22,7 +22,7 @@ export class BotResponse extends Schema.Class("BotResponse")({ * Response for bot creation - includes the plain token (only shown once). */ export class BotCreatedResponse extends Schema.Class("BotCreatedResponse")({ - data: Bot.Model.json, + data: Bot.Schema, token: Schema.String, // Plain token, only returned once on creation transactionId: TransactionId, }) {} @@ -31,21 +31,21 @@ export class BotCreatedResponse extends Schema.Class("BotCre * Response for listing bots. */ export class BotListResponse extends Schema.Class("BotListResponse")({ - data: Schema.Array(Bot.Model.json), + data: Schema.Array(Bot.Schema), }) {} /** * Response for listing bot commands. */ export class BotCommandListResponse extends Schema.Class("BotCommandListResponse")({ - data: Schema.Array(BotCommand.Model.json), + data: Schema.Array(BotCommand.Schema), }) {} /** * Public bot info for marketplace - includes install status and creator name. */ export const PublicBotInfo = Schema.Struct({ - ...Bot.Model.json.fields, + ...Bot.Schema.fields, isInstalled: Schema.Boolean, creatorName: Schema.String, }) diff --git a/packages/domain/src/rpc/channel-members.ts b/packages/domain/src/rpc/channel-members.ts index 34b50dec9..2b5d18e34 100644 --- a/packages/domain/src/rpc/channel-members.ts +++ b/packages/domain/src/rpc/channel-members.ts @@ -13,7 +13,7 @@ import { RequiredScopes } from "../scopes/required-scopes" * Contains the channel member data and a transaction ID for optimistic updates. */ export class ChannelMemberResponse extends Schema.Class("ChannelMemberResponse")({ - data: ChannelMember.Model.json, + data: ChannelMember.Schema, transactionId: TransactionId, }) {} @@ -104,7 +104,7 @@ export class ChannelMemberRpcs extends RpcGroup.make( id: ChannelMemberId, }).pipe( (s: any) => - Schema.Struct({ ...s.fields, ...(ChannelMember.Model.jsonUpdate as any).fields }) as any, + Schema.Struct({ ...s.fields, ...(ChannelMember.Patch as any).fields }) as any, ), success: ChannelMemberResponse, error: Schema.Union([ChannelMemberNotFoundError, UnauthorizedError, InternalServerError]), diff --git a/packages/domain/src/rpc/channel-sections.ts b/packages/domain/src/rpc/channel-sections.ts index 44e750e3a..26aa36ac1 100644 --- a/packages/domain/src/rpc/channel-sections.ts +++ b/packages/domain/src/rpc/channel-sections.ts @@ -13,7 +13,7 @@ import { RequiredScopes } from "../scopes/required-scopes" * Contains the section data and a transaction ID for optimistic updates. */ export class ChannelSectionResponse extends Schema.Class("ChannelSectionResponse")({ - data: ChannelSection.Model.json, + data: ChannelSection.Schema, transactionId: TransactionId, }) {} @@ -32,7 +32,7 @@ export class ChannelSectionNotFoundError extends Schema.TaggedErrorClass - Schema.Struct({ ...s.fields, ...(ChannelSection.Model.jsonUpdate as any).fields }) as any, + Schema.Struct({ ...s.fields, ...(ChannelSection.Patch as any).fields }) as any, ), success: ChannelSectionResponse, error: Schema.Union([ChannelSectionNotFoundError, UnauthorizedError, InternalServerError]), diff --git a/packages/domain/src/rpc/channel-webhooks.ts b/packages/domain/src/rpc/channel-webhooks.ts index 12ebf3e08..b247c2fbc 100644 --- a/packages/domain/src/rpc/channel-webhooks.ts +++ b/packages/domain/src/rpc/channel-webhooks.ts @@ -13,7 +13,7 @@ import { RequiredScopes } from "../scopes/required-scopes" * Contains the webhook data and a transaction ID for optimistic updates. */ export class ChannelWebhookResponse extends Schema.Class("ChannelWebhookResponse")({ - data: ChannelWebhook.Model.json, + data: ChannelWebhook.Schema, transactionId: TransactionId, }) {} @@ -23,7 +23,7 @@ export class ChannelWebhookResponse extends Schema.Class export class ChannelWebhookCreatedResponse extends Schema.Class( "ChannelWebhookCreatedResponse", )({ - data: ChannelWebhook.Model.json, + data: ChannelWebhook.Schema, token: Schema.String, // Plain token, only returned once on creation webhookUrl: Schema.String, // Full URL for the webhook transactionId: TransactionId, @@ -35,7 +35,7 @@ export class ChannelWebhookCreatedResponse extends Schema.Class( "ChannelWebhookListResponse", )({ - data: Schema.Array(ChannelWebhook.Model.json), + data: Schema.Array(ChannelWebhook.Schema), }) {} /** diff --git a/packages/domain/src/rpc/channels.ts b/packages/domain/src/rpc/channels.ts index 0aa63dcd4..a3a233a6a 100644 --- a/packages/domain/src/rpc/channels.ts +++ b/packages/domain/src/rpc/channels.ts @@ -28,7 +28,7 @@ import { RequiredScopes } from "../scopes/required-scopes" * Contains the channel data and a transaction ID for optimistic updates. */ export class ChannelResponse extends Schema.Class("ChannelResponse")({ - data: Channel.Model.json, + data: Channel.Schema, transactionId: TransactionId, }) {} @@ -71,7 +71,7 @@ export class CreateThreadRequest extends Schema.Class("Crea * Uses jsonCreate which includes optional id for optimistic updates. * Extended with addAllMembers option to auto-add all organization members. */ -export const CreateChannelRequest = Channel.Model.jsonCreate.pipe( +export const CreateChannelRequest = Channel.Create.pipe( Schema.fieldsAssign({ addAllMembers: Schema.optional(Schema.Boolean) }), ) @@ -112,7 +112,7 @@ export class ChannelRpcs extends RpcGroup.make( payload: Schema.Struct({ id: ChannelId, }).pipe( - (s: any) => Schema.Struct({ ...s.fields, ...(Channel.Model.jsonUpdate as any).fields }) as any, + (s: any) => Schema.Struct({ ...s.fields, ...(Channel.Patch as any).fields }) as any, ), success: ChannelResponse, error: Schema.Union([ChannelNotFoundError, UnauthorizedError, InternalServerError]), diff --git a/packages/domain/src/rpc/chat-sync.ts b/packages/domain/src/rpc/chat-sync.ts index 63cd5af63..38a16990a 100644 --- a/packages/domain/src/rpc/chat-sync.ts +++ b/packages/domain/src/rpc/chat-sync.ts @@ -17,27 +17,27 @@ import { RequiredScopes } from "../scopes/required-scopes" export class ChatSyncConnectionResponse extends Schema.Class( "ChatSyncConnectionResponse", )({ - data: ChatSyncConnection.Model.json, + data: ChatSyncConnection.Schema, transactionId: TransactionId, }) {} export class ChatSyncConnectionListResponse extends Schema.Class( "ChatSyncConnectionListResponse", )({ - data: Schema.Array(ChatSyncConnection.Model.json), + data: Schema.Array(ChatSyncConnection.Schema), }) {} export class ChatSyncChannelLinkResponse extends Schema.Class( "ChatSyncChannelLinkResponse", )({ - data: ChatSyncChannelLink.Model.json, + data: ChatSyncChannelLink.Schema, transactionId: TransactionId, }) {} export class ChatSyncChannelLinkListResponse extends Schema.Class( "ChatSyncChannelLinkListResponse", )({ - data: Schema.Array(ChatSyncChannelLink.Model.json), + data: Schema.Array(ChatSyncChannelLink.Schema), }) {} export class ChatSyncConnectionNotFoundError extends Schema.TaggedErrorClass()( diff --git a/packages/domain/src/rpc/connect-shares.ts b/packages/domain/src/rpc/connect-shares.ts index f4c81b244..a2047d063 100644 --- a/packages/domain/src/rpc/connect-shares.ts +++ b/packages/domain/src/rpc/connect-shares.ts @@ -14,28 +14,28 @@ import { AuthMiddleware } from "./middleware" import { RequiredScopes } from "../scopes/required-scopes" export class ConnectInviteResponse extends Schema.Class("ConnectInviteResponse")({ - data: ConnectInvite.Model.json, + data: ConnectInvite.Schema, transactionId: TransactionId, }) {} export class ConnectConversationResponse extends Schema.Class( "ConnectConversationResponse", )({ - data: ConnectConversation.Model.json, + data: ConnectConversation.Schema, transactionId: TransactionId, }) {} export class ConnectParticipantResponse extends Schema.Class( "ConnectParticipantResponse", )({ - data: ConnectParticipant.Model.json, + data: ConnectParticipant.Schema, transactionId: TransactionId, }) {} export class ConnectInviteListResponse extends Schema.Class( "ConnectInviteListResponse", )({ - data: Schema.Array(ConnectInvite.Model.json), + data: Schema.Array(ConnectInvite.Schema), }) {} export class ConnectWorkspaceSearchResult extends Schema.Class( diff --git a/packages/domain/src/rpc/custom-emojis.ts b/packages/domain/src/rpc/custom-emojis.ts index bcb7b5663..fb26cc6b6 100644 --- a/packages/domain/src/rpc/custom-emojis.ts +++ b/packages/domain/src/rpc/custom-emojis.ts @@ -11,7 +11,7 @@ import { RequiredScopes } from "../scopes/required-scopes" * Contains the custom emoji data and a transaction ID for optimistic updates. */ export class CustomEmojiResponse extends Schema.Class("CustomEmojiResponse")({ - data: CustomEmoji.Model.json, + data: CustomEmoji.Schema, transactionId: TransactionId, }) {} diff --git a/packages/domain/src/rpc/github-subscriptions.ts b/packages/domain/src/rpc/github-subscriptions.ts index 47bb6dac4..dbdbbc7af 100644 --- a/packages/domain/src/rpc/github-subscriptions.ts +++ b/packages/domain/src/rpc/github-subscriptions.ts @@ -15,7 +15,7 @@ import { RequiredScopes } from "../scopes/required-scopes" export class GitHubSubscriptionResponse extends Schema.Class( "GitHubSubscriptionResponse", )({ - data: GitHubSubscription.Model.json, + data: GitHubSubscription.Schema, transactionId: TransactionId, }) {} @@ -25,7 +25,7 @@ export class GitHubSubscriptionResponse extends Schema.Class( "GitHubSubscriptionListResponse", )({ - data: Schema.Array(GitHubSubscription.Model.json), + data: Schema.Array(GitHubSubscription.Schema), }) {} /** diff --git a/packages/domain/src/rpc/integration-requests.ts b/packages/domain/src/rpc/integration-requests.ts index 9ab17deab..bbb3e85c8 100644 --- a/packages/domain/src/rpc/integration-requests.ts +++ b/packages/domain/src/rpc/integration-requests.ts @@ -13,7 +13,7 @@ import { RequiredScopes } from "../scopes/required-scopes" export class IntegrationRequestResponse extends Schema.Class( "IntegrationRequestResponse", )({ - data: IntegrationRequest.Model.json, + data: IntegrationRequest.Schema, transactionId: TransactionId, }) {} diff --git a/packages/domain/src/rpc/invitations.ts b/packages/domain/src/rpc/invitations.ts index c1e6da8cd..795d6c246 100644 --- a/packages/domain/src/rpc/invitations.ts +++ b/packages/domain/src/rpc/invitations.ts @@ -12,7 +12,7 @@ import { RequiredScopes } from "../scopes/required-scopes" * Contains the invitation data and a transaction ID for optimistic updates. */ export class InvitationResponse extends Schema.Class("InvitationResponse")({ - data: Invitation.Model.json, + data: Invitation.Schema, transactionId: TransactionId, }) {} @@ -23,7 +23,7 @@ export class InvitationResponse extends Schema.Class("Invita export class InvitationBatchResult extends Schema.Class("InvitationBatchResult")({ email: Schema.String, success: Schema.Boolean, - data: Schema.optional(Invitation.Model.json), + data: Schema.optional(Invitation.Schema), error: Schema.optional(Schema.String), transactionId: Schema.optional(TransactionId), }) {} @@ -134,7 +134,7 @@ export class InvitationRpcs extends RpcGroup.make( Rpc.make("invitation.update", { payload: Schema.Struct({ id: InvitationId, - ...Invitation.Model.jsonUpdate.fields, + ...Invitation.Patch.fields, }), success: InvitationResponse, error: Schema.Union([InvitationNotFoundError, UnauthorizedError, InternalServerError]), diff --git a/packages/domain/src/rpc/message-reactions.ts b/packages/domain/src/rpc/message-reactions.ts index 451cbe742..79de287f0 100644 --- a/packages/domain/src/rpc/message-reactions.ts +++ b/packages/domain/src/rpc/message-reactions.ts @@ -14,7 +14,7 @@ import { RequiredScopes } from "../scopes/required-scopes" */ export class MessageReactionResponse extends Schema.Class("MessageReactionResponse")( { - data: MessageReaction.Model.json, + data: MessageReaction.Schema, transactionId: TransactionId, }, ) {} @@ -52,7 +52,7 @@ export class MessageReactionRpcs extends RpcGroup.make( }), success: Schema.Struct({ wasCreated: Schema.Boolean, - data: Schema.optional(MessageReaction.Model.json), + data: Schema.optional(MessageReaction.Schema), transactionId: TransactionId, }), error: Schema.Union([MessageNotFoundError, UnauthorizedError, InternalServerError]), @@ -95,7 +95,7 @@ export class MessageReactionRpcs extends RpcGroup.make( Rpc.make("messageReaction.update", { payload: Schema.Struct({ id: MessageReactionId, - ...MessageReaction.Model.jsonUpdate.fields, + ...MessageReaction.Patch.fields, }), success: MessageReactionResponse, error: Schema.Union([MessageReactionNotFoundError, UnauthorizedError, InternalServerError]), diff --git a/packages/domain/src/rpc/messages.ts b/packages/domain/src/rpc/messages.ts index 37c456bcc..3dd6416dd 100644 --- a/packages/domain/src/rpc/messages.ts +++ b/packages/domain/src/rpc/messages.ts @@ -17,7 +17,7 @@ export { MessageNotFoundError } from "../errors" * Contains the message data and a transaction ID for optimistic updates. */ export class MessageResponse extends Schema.Class("MessageResponse")({ - data: Message.Model.json, + data: Message.Schema, transactionId: TransactionId, }) {} @@ -97,7 +97,7 @@ export class MessageRpcs extends RpcGroup.make( Rpc.make("message.update", { payload: Schema.Struct({ id: MessageId, - }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(Message.JsonUpdate as any).fields }) as any), + }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(Message.Patch as any).fields }) as any), success: MessageResponse, error: Schema.Union([ MessageNotFoundError, diff --git a/packages/domain/src/rpc/notifications.ts b/packages/domain/src/rpc/notifications.ts index 05b39f0d6..f4b4e0f4e 100644 --- a/packages/domain/src/rpc/notifications.ts +++ b/packages/domain/src/rpc/notifications.ts @@ -12,7 +12,7 @@ import { RequiredScopes } from "../scopes/required-scopes" * Contains the notification data and a transaction ID for optimistic updates. */ export class NotificationResponse extends Schema.Class("NotificationResponse")({ - data: Notification.Model.json, + data: Notification.Schema, transactionId: TransactionId, }) {} @@ -40,7 +40,7 @@ export class NotificationRpcs extends RpcGroup.make( * @throws InternalServerError for unexpected errors */ Rpc.make("notification.create", { - payload: Notification.Model.jsonCreate, + payload: Notification.Create, success: NotificationResponse, error: Schema.Union([UnauthorizedError, InternalServerError]), }) @@ -62,7 +62,7 @@ export class NotificationRpcs extends RpcGroup.make( Rpc.make("notification.update", { payload: Schema.Struct({ id: NotificationId, - ...Notification.Model.jsonUpdate.fields, + ...Notification.Patch.fields, }), success: NotificationResponse, error: Schema.Union([NotificationNotFoundError, UnauthorizedError, InternalServerError]), diff --git a/packages/domain/src/rpc/organization-members.ts b/packages/domain/src/rpc/organization-members.ts index 4ba390e1b..099171b98 100644 --- a/packages/domain/src/rpc/organization-members.ts +++ b/packages/domain/src/rpc/organization-members.ts @@ -15,7 +15,7 @@ import { OrganizationNotFoundError } from "./organizations" export class OrganizationMemberResponse extends Schema.Class( "OrganizationMemberResponse", )({ - data: OrganizationMember.Model.json, + data: OrganizationMember.Schema, transactionId: TransactionId, }) {} @@ -75,7 +75,7 @@ export class OrganizationMemberRpcs extends RpcGroup.make( * @throws InternalServerError for unexpected errors */ Rpc.make("organizationMember.create", { - payload: OrganizationMember.Model.jsonCreate, + payload: OrganizationMember.Create, success: OrganizationMemberResponse, error: Schema.Union([OrganizationNotFoundError, UnauthorizedError, InternalServerError]), }) @@ -97,7 +97,7 @@ export class OrganizationMemberRpcs extends RpcGroup.make( Rpc.make("organizationMember.update", { payload: Schema.Struct({ id: OrganizationMemberId, - ...OrganizationMember.Model.jsonUpdate.fields, + ...OrganizationMember.Patch.fields, }), success: OrganizationMemberResponse, error: Schema.Union([OrganizationMemberNotFoundError, UnauthorizedError, InternalServerError]), diff --git a/packages/domain/src/rpc/organizations.ts b/packages/domain/src/rpc/organizations.ts index d3d3e7a32..9a35b5096 100644 --- a/packages/domain/src/rpc/organizations.ts +++ b/packages/domain/src/rpc/organizations.ts @@ -12,7 +12,7 @@ import { RequiredScopes } from "../scopes/required-scopes" * Contains the organization data and a transaction ID for optimistic updates. */ export class OrganizationResponse extends Schema.Class("OrganizationResponse")({ - data: Organization.Model.json, + data: Organization.Schema, transactionId: TransactionId, }) {} @@ -70,7 +70,7 @@ export class PublicOrganizationInfo extends Schema.Class export class OrganizationRpcs extends RpcGroup.make( Rpc.make("organization.create", { - payload: Organization.Model.jsonCreate, + payload: Organization.Create, success: OrganizationResponse, error: Schema.Union([OrganizationSlugAlreadyExistsError, UnauthorizedError, InternalServerError]), }) @@ -82,7 +82,7 @@ export class OrganizationRpcs extends RpcGroup.make( id: OrganizationId, }).pipe( (s: any) => - Schema.Struct({ ...s.fields, ...(Organization.Model.jsonUpdate as any).fields }) as any, + Schema.Struct({ ...s.fields, ...(Organization.Patch as any).fields }) as any, ), success: OrganizationResponse, error: Schema.Union([ diff --git a/packages/domain/src/rpc/pinned-messages.ts b/packages/domain/src/rpc/pinned-messages.ts index 3237c9d7b..de442d75a 100644 --- a/packages/domain/src/rpc/pinned-messages.ts +++ b/packages/domain/src/rpc/pinned-messages.ts @@ -13,7 +13,7 @@ import { RequiredScopes } from "../scopes/required-scopes" * Contains the pinned message data and a transaction ID for optimistic updates. */ export class PinnedMessageResponse extends Schema.Class("PinnedMessageResponse")({ - data: PinnedMessage.Model.json, + data: PinnedMessage.Schema, transactionId: TransactionId, }) {} @@ -97,7 +97,7 @@ export class PinnedMessageRpcs extends RpcGroup.make( Rpc.make("pinnedMessage.update", { payload: Schema.Struct({ id: PinnedMessageId, - ...PinnedMessage.Model.jsonUpdate.fields, + ...PinnedMessage.Patch.fields, }), success: PinnedMessageResponse, error: Schema.Union([PinnedMessageNotFoundError, UnauthorizedError, InternalServerError]), diff --git a/packages/domain/src/rpc/rss-subscriptions.ts b/packages/domain/src/rpc/rss-subscriptions.ts index 3a70991cc..7812218a8 100644 --- a/packages/domain/src/rpc/rss-subscriptions.ts +++ b/packages/domain/src/rpc/rss-subscriptions.ts @@ -10,7 +10,7 @@ import { RequiredScopes } from "../scopes/required-scopes" export class RssSubscriptionResponse extends Schema.Class("RssSubscriptionResponse")( { - data: RssSubscription.Model.json, + data: RssSubscription.Schema, transactionId: TransactionId, }, ) {} @@ -18,7 +18,7 @@ export class RssSubscriptionResponse extends Schema.Class( "RssSubscriptionListResponse", )({ - data: Schema.Array(RssSubscription.Model.json), + data: Schema.Array(RssSubscription.Schema), }) {} export class RssSubscriptionNotFoundError extends Schema.TaggedErrorClass()( diff --git a/packages/domain/src/rpc/typing-indicators.ts b/packages/domain/src/rpc/typing-indicators.ts index 1c814132a..4314a5148 100644 --- a/packages/domain/src/rpc/typing-indicators.ts +++ b/packages/domain/src/rpc/typing-indicators.ts @@ -13,7 +13,7 @@ import { RequiredScopes } from "../scopes/required-scopes" */ export class TypingIndicatorResponse extends Schema.Class("TypingIndicatorResponse")( { - data: TypingIndicator.Model.json, + data: TypingIndicator.Schema, transactionId: TransactionId, }, ) {} diff --git a/packages/domain/src/rpc/user-presence-status.ts b/packages/domain/src/rpc/user-presence-status.ts index bad80691b..d58f70687 100644 --- a/packages/domain/src/rpc/user-presence-status.ts +++ b/packages/domain/src/rpc/user-presence-status.ts @@ -15,7 +15,7 @@ import { RequiredScopes } from "../scopes/required-scopes" export class UserPresenceStatusResponse extends Schema.Class( "UserPresenceStatusResponse", )({ - data: UserPresenceStatus.Model.json, + data: UserPresenceStatus.Schema, transactionId: TransactionId, }) {} @@ -63,12 +63,12 @@ export class UserPresenceStatusRpcs extends RpcGroup.make( */ Rpc.make("userPresenceStatus.update", { payload: Schema.Struct({ - status: Schema.optional(UserPresenceStatus.Model.json.fields.status), + status: Schema.optional(UserPresenceStatus.Schema.fields.status), customMessage: Schema.optional(Schema.NullOr(Schema.String)), statusEmoji: Schema.optional(Schema.NullOr(Schema.String)), statusExpiresAt: Schema.optional(Schema.NullOr(JsonDate)), activeChannelId: Schema.optional( - Schema.NullOr(UserPresenceStatus.Model.json.fields.activeChannelId), + Schema.NullOr(UserPresenceStatus.Schema.fields.activeChannelId), ), suppressNotifications: Schema.optional(Schema.Boolean), }), diff --git a/packages/domain/src/rpc/users.ts b/packages/domain/src/rpc/users.ts index c6da10c78..2bc318883 100644 --- a/packages/domain/src/rpc/users.ts +++ b/packages/domain/src/rpc/users.ts @@ -13,7 +13,7 @@ import { RequiredScopes } from "../scopes/required-scopes" * Contains the user data and a transaction ID for optimistic updates. */ export class UserResponse extends Schema.Class("UserResponse")({ - data: User.Model.json, + data: User.Schema, transactionId: TransactionId, }) {} @@ -58,7 +58,7 @@ export class UserRpcs extends RpcGroup.make( Rpc.make("user.update", { payload: Schema.Struct({ id: UserId, - }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(User.Model.jsonUpdate as any).fields }) as any), + }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(User.Patch as any).fields }) as any), success: UserResponse, error: Schema.Union([UserNotFoundError, UnauthorizedError, InternalServerError]), }) From 60f92a8ffa0db07b9cc833725993471c6712c93c Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 17 Mar 2026 21:44:58 +0100 Subject: [PATCH 34/34] fix: replace Promise.withResolvers with manual construction for CI compat Plus pending Effect v4 migration changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 75 +- apps/backend/src/index.ts | 6 +- apps/backend/src/rpc/middleware/auth.ts | 11 +- .../services/auth-redemption-store.test.ts | 8 +- .../src/services/channel-access-sync.ts | 3 +- .../services/connect-conversation-service.ts | 2 + .../src/services/integration-token-service.ts | 2 - .../src/services/message-outbox-dispatcher.ts | 2 + .../workflows/message-notification-handler.ts | 4 +- apps/cluster/vitest.config.ts | 3 + apps/docs/src/routeTree.gen.ts | 104 +- apps/web/src/routeTree.gen.ts | 2488 ++++++++--------- apps/web/src/utils/attachment-url.ts | 5 +- .../src/repositories/attachment-repo.ts | 12 +- .../src/repositories/bot-command-repo.ts | 12 +- .../backend-core/src/repositories/bot-repo.ts | 26 +- .../src/repositories/channel-repo.ts | 12 +- .../src/repositories/custom-emoji-repo.ts | 12 +- .../src/repositories/invitation-repo.ts | 12 +- .../src/repositories/message-repo.ts | 12 +- .../src/repositories/user-repo.ts | 12 +- packages/db/src/services/repository.ts | 19 +- .../models/chat-sync-channel-link-model.ts | 4 +- .../models/integration-connection-model.ts | 9 +- packages/domain/src/models/utils.ts | 15 +- packages/domain/src/rpc/channel-members.ts | 5 +- packages/domain/src/rpc/channel-sections.ts | 5 +- packages/domain/src/rpc/channels.ts | 6 +- packages/domain/src/rpc/messages.ts | 2 +- packages/domain/src/rpc/organizations.ts | 5 +- .../domain/src/rpc/user-presence-status.ts | 4 +- packages/domain/src/rpc/users.ts | 2 +- vitest.config.ts | 1 + 33 files changed, 1433 insertions(+), 1467 deletions(-) create mode 100644 apps/cluster/vitest.config.ts diff --git a/CLAUDE.md b/CLAUDE.md index b8180e4e5..651296ca0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -208,25 +208,28 @@ Without both changes, Electric sync requests for the new table will be rejected ## Effect-TS Best Practices -> **Skill Available**: Run `/effect-best-practices` for comprehensive Effect-TS patterns. The skill auto-activates when writing Effect.Service, Schema.TaggedError, Layer composition, or effect-atom code. +> **Skill Available**: Run `/effect-best-practices` for comprehensive Effect-TS patterns. The skill auto-activates when writing ServiceMap.Service, Schema.TaggedError, Layer composition, or effect-atom code. -### Always Use `Effect.Service` Instead of `Context.Tag` +### Always Use `ServiceMap.Service` Instead of `Context.Tag` -**ALWAYS** prefer `Effect.Service` over `Context.Tag` for defining services. Effect.Service provides built-in `Default` layer, automatic accessors, and proper dependency declaration. +**ALWAYS** prefer `ServiceMap.Service` (from `effect`) over `Context.Tag` for defining services. `ServiceMap.Service` with a `make` option stores the constructor effect on the class. You must define the layer explicitly using `Layer.effect`. ```typescript -// ✅ CORRECT - Use Effect.Service -export class MyService extends Effect.Service()("MyService", { - accessors: true, - effect: Effect.gen(function* () { +// ✅ CORRECT - Use ServiceMap.Service with make and explicit layer +import { ServiceMap, Effect, Layer } from "effect" + +export class MyService extends ServiceMap.Service()("MyService", { + make: Effect.gen(function* () { // ... implementation return { /* methods */ } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} -// Usage: MyService.Default, yield* MyService +// Usage: MyService.layer, yield* MyService ``` ```typescript @@ -237,7 +240,7 @@ export class MyService extends Context.Tag("MyService")< /* shape */ } >() { - static Default = Layer.effect( + static layer = Layer.effect( this, Effect.gen(function* () { /* ... */ @@ -246,50 +249,58 @@ export class MyService extends Context.Tag("MyService")< } ``` +```typescript +// ❌ WRONG - Don't use v3 Effect.Service pattern +export class MyService extends Effect.Service()("MyService", { + accessors: true, + effect: Effect.gen(function* () { + /* ... */ + }), +}) {} +``` + **When Context.Tag is acceptable:** - Infrastructure layers with runtime injection (e.g., Cloudflare KV namespace, worker bindings) - Factory patterns where the resource is provided externally at runtime -### Use `dependencies` Array in Effect.Service +### Wire Dependencies with `Layer.provide` -**ALWAYS** declare service dependencies in the `dependencies` array when using `Effect.Service`. This ensures proper layer composition and avoids "leaked dependencies" that require manual `Layer.provide` calls at the usage site. +Wire service dependencies using `Layer.provide` on the layer. The v3 `dependencies` array no longer exists. ```typescript -// ✅ CORRECT - Dependencies declared in the service -export class MyService extends Effect.Service()("MyService", { - accessors: true, - dependencies: [DatabaseService.Default, CacheService.Default], - effect: Effect.gen(function* () { +// ✅ CORRECT - Dependencies wired via Layer.provide on the layer +export class MyService extends ServiceMap.Service()("MyService", { + make: Effect.gen(function* () { const db = yield* DatabaseService const cache = yield* CacheService // ... implementation + return { + /* methods */ + } }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(DatabaseService.layer), + Layer.provide(CacheService.layer), + ) +} -// Usage is simple - MyService.Default includes all dependencies -const MainLive = Layer.mergeAll(MyService.Default, OtherService.Default) +// Usage is simple - MyService.layer includes all dependencies +const MainLive = Layer.mergeAll(MyService.layer, OtherService.layer) ``` ```typescript -// ❌ WRONG - Dependencies leaked to usage site +// ❌ WRONG - v3 dependencies array no longer exists export class MyService extends Effect.Service()("MyService", { - accessors: true, + dependencies: [DatabaseService.Default, CacheService.Default], effect: Effect.gen(function* () { - const db = yield* DatabaseService - const cache = yield* CacheService - // ... implementation + /* ... */ }), }) {} - -// Now every usage site must manually wire dependencies -const MainLive = Layer.mergeAll( - MyService.Default.pipe(Layer.provide(DatabaseService.Default), Layer.provide(CacheService.Default)), - OtherService.Default, -) ``` -**When it's acceptable to omit dependencies:** +**When it's acceptable to omit `Layer.provide`:** - Infrastructure layers that are globally provided (e.g., Redis, Database) may be intentionally "leaked" to be provided once at the application root - When a dependency is explicitly meant to be provided by the consumer diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index f7116e032..6c20b46e5 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -249,5 +249,9 @@ const ServerLayer = HttpRouter.serve(AllRoutes).pipe( ), ) -// TODO: Layer has leaked dependencies — fix service layer wiring so this cast is unnecessary +// The `as never` cast is required because ChatSyncCoreWorkerMake (in chat-sync-core-worker.ts) +// is explicitly typed as Effect<..., unknown, unknown> to break a circular type dependency. +// Those `unknown` types propagate through DiscordSyncWorkerLayer -> ServiceLive -> MainLive -> ServerLayer, +// causing TypeScript to collapse the layer's type parameters to `unknown`. +// All actual dependencies are wired correctly at runtime. ServerLayer.pipe(Layer.launch as never, BunRuntime.runMain) diff --git a/apps/backend/src/rpc/middleware/auth.ts b/apps/backend/src/rpc/middleware/auth.ts index 71f01501c..cebcc1084 100644 --- a/apps/backend/src/rpc/middleware/auth.ts +++ b/apps/backend/src/rpc/middleware/auth.ts @@ -74,10 +74,7 @@ export const AuthMiddlewareLive = Layer.effect( const bot = botOption.value - // TODO: v4 migration — Set the bot's declared scopes for authorization - // In v4, CurrentBotScopes is a ServiceMap.Service, needs different pattern - const _botApiScopes = new Set(bot.scopes ?? []) as ReadonlySet - void _botApiScopes // Will be used when service provision is wired + const botApiScopes = new Set(bot.scopes ?? []) as ReadonlySet // Get the bot's user from users table const userOption = yield* userRepo.findById(bot.userId).pipe( @@ -115,7 +112,11 @@ export const AuthMiddlewareLive = Layer.effect( settings: user.settings, } - return yield* Effect.provideService(effect, CurrentUser.Context, botUser) + return yield* Effect.provideService( + Effect.provideService(effect, CurrentUser.Context, botUser), + CurrentBotScopes, + Option.some(botApiScopes), + ) } // No valid authentication provided diff --git a/apps/backend/src/services/auth-redemption-store.test.ts b/apps/backend/src/services/auth-redemption-store.test.ts index 1b80675c2..7b0c8c6a7 100644 --- a/apps/backend/src/services/auth-redemption-store.test.ts +++ b/apps/backend/src/services/auth-redemption-store.test.ts @@ -112,7 +112,13 @@ const makeStore = async () => describe("AuthRedemptionStore", () => { it("deduplicates concurrent redemptions and calls WorkOS once", async () => { const store = await makeStore() - const gate = Promise.withResolvers() + let resolveGate: () => void + const gate = { + promise: new Promise((r) => { + resolveGate = r + }), + resolve: () => resolveGate(), + } let calls = 0 const response = makeResponse() const exchange = Effect.gen(function* () { diff --git a/apps/backend/src/services/channel-access-sync.ts b/apps/backend/src/services/channel-access-sync.ts index 47c2fbd93..acceafabd 100644 --- a/apps/backend/src/services/channel-access-sync.ts +++ b/apps/backend/src/services/channel-access-sync.ts @@ -2,6 +2,7 @@ import { and, eq, isNull, notInArray, schema } from "@hazel/db" import type { ChannelId, ConnectConversationId, OrganizationId, UserId } from "@hazel/schema" import { ServiceMap, Effect, Layer } from "effect" import { transactionAwareExecute } from "../lib/transaction-aware-execute" +import { DatabaseLive } from "./database" export class ChannelAccessSyncService extends ServiceMap.Service()( "ChannelAccessSyncService", @@ -394,5 +395,5 @@ export class ChannelAccessSyncService extends ServiceMap.Service()( @@ -323,6 +324,7 @@ export class ConnectConversationService extends ServiceMap.Service rootRouteImport, + id: "/$", + path: "/$", + getParentRoute: () => rootRouteImport, } as any) const ApiSearchRoute = ApiSearchRouteImport.update({ - id: '/api/search', - path: '/api/search', - getParentRoute: () => rootRouteImport, + id: "/api/search", + path: "/api/search", + getParentRoute: () => rootRouteImport, } as any) export interface FileRoutesByFullPath { - '/$': typeof SplatRoute - '/api/search': typeof ApiSearchRoute + "/$": typeof SplatRoute + "/api/search": typeof ApiSearchRoute } export interface FileRoutesByTo { - '/$': typeof SplatRoute - '/api/search': typeof ApiSearchRoute + "/$": typeof SplatRoute + "/api/search": typeof ApiSearchRoute } export interface FileRoutesById { - __root__: typeof rootRouteImport - '/$': typeof SplatRoute - '/api/search': typeof ApiSearchRoute + __root__: typeof rootRouteImport + "/$": typeof SplatRoute + "/api/search": typeof ApiSearchRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/$' | '/api/search' - fileRoutesByTo: FileRoutesByTo - to: '/$' | '/api/search' - id: '__root__' | '/$' | '/api/search' - fileRoutesById: FileRoutesById + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: "/$" | "/api/search" + fileRoutesByTo: FileRoutesByTo + to: "/$" | "/api/search" + id: "__root__" | "/$" | "/api/search" + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - SplatRoute: typeof SplatRoute - ApiSearchRoute: typeof ApiSearchRoute + SplatRoute: typeof SplatRoute + ApiSearchRoute: typeof ApiSearchRoute } -declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/$': { - id: '/$' - path: '/$' - fullPath: '/$' - preLoaderRoute: typeof SplatRouteImport - parentRoute: typeof rootRouteImport - } - '/api/search': { - id: '/api/search' - path: '/api/search' - fullPath: '/api/search' - preLoaderRoute: typeof ApiSearchRouteImport - parentRoute: typeof rootRouteImport - } - } +declare module "@tanstack/react-router" { + interface FileRoutesByPath { + "/$": { + id: "/$" + path: "/$" + fullPath: "/$" + preLoaderRoute: typeof SplatRouteImport + parentRoute: typeof rootRouteImport + } + "/api/search": { + id: "/api/search" + path: "/api/search" + fullPath: "/api/search" + preLoaderRoute: typeof ApiSearchRouteImport + parentRoute: typeof rootRouteImport + } + } } const rootRouteChildren: RootRouteChildren = { - SplatRoute: SplatRoute, - ApiSearchRoute: ApiSearchRoute, + SplatRoute: SplatRoute, + ApiSearchRoute: ApiSearchRoute, } -export const routeTree = rootRouteImport - ._addFileChildren(rootRouteChildren) - ._addFileTypes() +export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes() -import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' -declare module '@tanstack/react-start' { - interface Register { - ssr: true - router: Awaited> - } +import type { getRouter } from "./router.tsx" +import type { createStart } from "@tanstack/react-start" +declare module "@tanstack/react-start" { + interface Register { + ssr: true + router: Awaited> + } } diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 97e3cf499..d4eb0d2d4 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -8,1420 +8,1362 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { Route as rootRouteImport } from './routes/__root' -import { Route as DevLayoutRouteImport } from './routes/_dev/layout' -import { Route as AppLayoutRouteImport } from './routes/_app/layout' -import { Route as AppIndexRouteImport } from './routes/_app/index' -import { Route as JoinSlugRouteImport } from './routes/join/$slug' -import { Route as AuthLoginRouteImport } from './routes/auth/login' -import { Route as AuthDesktopLoginRouteImport } from './routes/auth/desktop-login' -import { Route as AuthDesktopCallbackRouteImport } from './routes/auth/desktop-callback' -import { Route as AuthCallbackRouteImport } from './routes/auth/callback' -import { Route as DevUiLayoutRouteImport } from './routes/_dev/ui/layout' -import { Route as AppOrgSlugLayoutRouteImport } from './routes/_app/$orgSlug/layout' -import { Route as DevEmbedsIndexRouteImport } from './routes/dev/embeds/index' -import { Route as AppSelectOrganizationIndexRouteImport } from './routes/_app/select-organization/index' -import { Route as AppOnboardingIndexRouteImport } from './routes/_app/onboarding/index' -import { Route as AppOrgSlugIndexRouteImport } from './routes/_app/$orgSlug/index' -import { Route as DevEmbedsRailwayRouteImport } from './routes/dev/embeds/railway' -import { Route as DevEmbedsOpenstatusRouteImport } from './routes/dev/embeds/openstatus' -import { Route as DevEmbedsGithubRouteImport } from './routes/dev/embeds/github' -import { Route as DevEmbedsDemoRouteImport } from './routes/dev/embeds/demo' -import { Route as DevUiAgentStepsRouteImport } from './routes/_dev/ui/agent-steps' -import { Route as AppOnboardingSetupOrganizationRouteImport } from './routes/_app/onboarding/setup-organization' -import { Route as AppOrgSlugSettingsLayoutRouteImport } from './routes/_app/$orgSlug/settings/layout' -import { Route as AppOrgSlugNotificationsLayoutRouteImport } from './routes/_app/$orgSlug/notifications/layout' -import { Route as AppOrgSlugMySettingsLayoutRouteImport } from './routes/_app/$orgSlug/my-settings/layout' -import { Route as AppOrgSlugSettingsIndexRouteImport } from './routes/_app/$orgSlug/settings/index' -import { Route as AppOrgSlugNotificationsIndexRouteImport } from './routes/_app/$orgSlug/notifications/index' -import { Route as AppOrgSlugMySettingsIndexRouteImport } from './routes/_app/$orgSlug/my-settings/index' -import { Route as AppOrgSlugChatIndexRouteImport } from './routes/_app/$orgSlug/chat/index' -import { Route as AppOrgSlugSettingsTeamRouteImport } from './routes/_app/$orgSlug/settings/team' -import { Route as AppOrgSlugSettingsInvitationsRouteImport } from './routes/_app/$orgSlug/settings/invitations' -import { Route as AppOrgSlugSettingsDebugRouteImport } from './routes/_app/$orgSlug/settings/debug' -import { Route as AppOrgSlugSettingsCustomEmojisRouteImport } from './routes/_app/$orgSlug/settings/custom-emojis' -import { Route as AppOrgSlugSettingsConnectInvitesRouteImport } from './routes/_app/$orgSlug/settings/connect-invites' -import { Route as AppOrgSlugSettingsAuthenticationRouteImport } from './routes/_app/$orgSlug/settings/authentication' -import { Route as AppOrgSlugProfileUserIdRouteImport } from './routes/_app/$orgSlug/profile/$userId' -import { Route as AppOrgSlugNotificationsThreadsRouteImport } from './routes/_app/$orgSlug/notifications/threads' -import { Route as AppOrgSlugNotificationsGeneralRouteImport } from './routes/_app/$orgSlug/notifications/general' -import { Route as AppOrgSlugNotificationsDmsRouteImport } from './routes/_app/$orgSlug/notifications/dms' -import { Route as AppOrgSlugMySettingsProfileRouteImport } from './routes/_app/$orgSlug/my-settings/profile' -import { Route as AppOrgSlugMySettingsNotificationsRouteImport } from './routes/_app/$orgSlug/my-settings/notifications' -import { Route as AppOrgSlugMySettingsLinkedAccountsRouteImport } from './routes/_app/$orgSlug/my-settings/linked-accounts' -import { Route as AppOrgSlugMySettingsDesktopRouteImport } from './routes/_app/$orgSlug/my-settings/desktop' -import { Route as AppOrgSlugChatIdRouteImport } from './routes/_app/$orgSlug/chat/$id' -import { Route as AppOrgSlugSettingsIntegrationsLayoutRouteImport } from './routes/_app/$orgSlug/settings/integrations/layout' -import { Route as AppOrgSlugSettingsChatSyncLayoutRouteImport } from './routes/_app/$orgSlug/settings/chat-sync/layout' -import { Route as AppOrgSlugSettingsIntegrationsIndexRouteImport } from './routes/_app/$orgSlug/settings/integrations/index' -import { Route as AppOrgSlugSettingsChatSyncIndexRouteImport } from './routes/_app/$orgSlug/settings/chat-sync/index' -import { Route as AppOrgSlugChatIdIndexRouteImport } from './routes/_app/$orgSlug/chat/$id/index' -import { Route as AppOrgSlugSettingsIntegrationsYourAppsRouteImport } from './routes/_app/$orgSlug/settings/integrations/your-apps' -import { Route as AppOrgSlugSettingsIntegrationsMarketplaceRouteImport } from './routes/_app/$orgSlug/settings/integrations/marketplace' -import { Route as AppOrgSlugSettingsIntegrationsInstalledRouteImport } from './routes/_app/$orgSlug/settings/integrations/installed' -import { Route as AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport } from './routes/_app/$orgSlug/settings/integrations/$integrationId' -import { Route as AppOrgSlugSettingsChatSyncConnectionIdRouteImport } from './routes/_app/$orgSlug/settings/chat-sync/$connectionId' -import { Route as AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/layout' -import { Route as AppOrgSlugChatIdFilesIndexRouteImport } from './routes/_app/$orgSlug/chat/$id/files/index' -import { Route as AppOrgSlugChannelsChannelIdSettingsIndexRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/index' -import { Route as AppOrgSlugChatIdFilesMediaRouteImport } from './routes/_app/$orgSlug/chat/$id/files/media' -import { Route as AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/overview' -import { Route as AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/integrations' -import { Route as AppOrgSlugChannelsChannelIdSettingsConnectRouteImport } from './routes/_app/$orgSlug/channels/$channelId/settings/connect' +import { Route as rootRouteImport } from "./routes/__root" +import { Route as DevLayoutRouteImport } from "./routes/_dev/layout" +import { Route as AppLayoutRouteImport } from "./routes/_app/layout" +import { Route as AppIndexRouteImport } from "./routes/_app/index" +import { Route as JoinSlugRouteImport } from "./routes/join/$slug" +import { Route as AuthLoginRouteImport } from "./routes/auth/login" +import { Route as AuthDesktopLoginRouteImport } from "./routes/auth/desktop-login" +import { Route as AuthDesktopCallbackRouteImport } from "./routes/auth/desktop-callback" +import { Route as AuthCallbackRouteImport } from "./routes/auth/callback" +import { Route as DevUiLayoutRouteImport } from "./routes/_dev/ui/layout" +import { Route as AppOrgSlugLayoutRouteImport } from "./routes/_app/$orgSlug/layout" +import { Route as DevEmbedsIndexRouteImport } from "./routes/dev/embeds/index" +import { Route as AppSelectOrganizationIndexRouteImport } from "./routes/_app/select-organization/index" +import { Route as AppOnboardingIndexRouteImport } from "./routes/_app/onboarding/index" +import { Route as AppOrgSlugIndexRouteImport } from "./routes/_app/$orgSlug/index" +import { Route as DevEmbedsRailwayRouteImport } from "./routes/dev/embeds/railway" +import { Route as DevEmbedsOpenstatusRouteImport } from "./routes/dev/embeds/openstatus" +import { Route as DevEmbedsGithubRouteImport } from "./routes/dev/embeds/github" +import { Route as DevEmbedsDemoRouteImport } from "./routes/dev/embeds/demo" +import { Route as DevUiAgentStepsRouteImport } from "./routes/_dev/ui/agent-steps" +import { Route as AppOnboardingSetupOrganizationRouteImport } from "./routes/_app/onboarding/setup-organization" +import { Route as AppOrgSlugSettingsLayoutRouteImport } from "./routes/_app/$orgSlug/settings/layout" +import { Route as AppOrgSlugNotificationsLayoutRouteImport } from "./routes/_app/$orgSlug/notifications/layout" +import { Route as AppOrgSlugMySettingsLayoutRouteImport } from "./routes/_app/$orgSlug/my-settings/layout" +import { Route as AppOrgSlugSettingsIndexRouteImport } from "./routes/_app/$orgSlug/settings/index" +import { Route as AppOrgSlugNotificationsIndexRouteImport } from "./routes/_app/$orgSlug/notifications/index" +import { Route as AppOrgSlugMySettingsIndexRouteImport } from "./routes/_app/$orgSlug/my-settings/index" +import { Route as AppOrgSlugChatIndexRouteImport } from "./routes/_app/$orgSlug/chat/index" +import { Route as AppOrgSlugSettingsTeamRouteImport } from "./routes/_app/$orgSlug/settings/team" +import { Route as AppOrgSlugSettingsInvitationsRouteImport } from "./routes/_app/$orgSlug/settings/invitations" +import { Route as AppOrgSlugSettingsDebugRouteImport } from "./routes/_app/$orgSlug/settings/debug" +import { Route as AppOrgSlugSettingsCustomEmojisRouteImport } from "./routes/_app/$orgSlug/settings/custom-emojis" +import { Route as AppOrgSlugSettingsConnectInvitesRouteImport } from "./routes/_app/$orgSlug/settings/connect-invites" +import { Route as AppOrgSlugSettingsAuthenticationRouteImport } from "./routes/_app/$orgSlug/settings/authentication" +import { Route as AppOrgSlugProfileUserIdRouteImport } from "./routes/_app/$orgSlug/profile/$userId" +import { Route as AppOrgSlugNotificationsThreadsRouteImport } from "./routes/_app/$orgSlug/notifications/threads" +import { Route as AppOrgSlugNotificationsGeneralRouteImport } from "./routes/_app/$orgSlug/notifications/general" +import { Route as AppOrgSlugNotificationsDmsRouteImport } from "./routes/_app/$orgSlug/notifications/dms" +import { Route as AppOrgSlugMySettingsProfileRouteImport } from "./routes/_app/$orgSlug/my-settings/profile" +import { Route as AppOrgSlugMySettingsNotificationsRouteImport } from "./routes/_app/$orgSlug/my-settings/notifications" +import { Route as AppOrgSlugMySettingsLinkedAccountsRouteImport } from "./routes/_app/$orgSlug/my-settings/linked-accounts" +import { Route as AppOrgSlugMySettingsDesktopRouteImport } from "./routes/_app/$orgSlug/my-settings/desktop" +import { Route as AppOrgSlugChatIdRouteImport } from "./routes/_app/$orgSlug/chat/$id" +import { Route as AppOrgSlugSettingsIntegrationsLayoutRouteImport } from "./routes/_app/$orgSlug/settings/integrations/layout" +import { Route as AppOrgSlugSettingsChatSyncLayoutRouteImport } from "./routes/_app/$orgSlug/settings/chat-sync/layout" +import { Route as AppOrgSlugSettingsIntegrationsIndexRouteImport } from "./routes/_app/$orgSlug/settings/integrations/index" +import { Route as AppOrgSlugSettingsChatSyncIndexRouteImport } from "./routes/_app/$orgSlug/settings/chat-sync/index" +import { Route as AppOrgSlugChatIdIndexRouteImport } from "./routes/_app/$orgSlug/chat/$id/index" +import { Route as AppOrgSlugSettingsIntegrationsYourAppsRouteImport } from "./routes/_app/$orgSlug/settings/integrations/your-apps" +import { Route as AppOrgSlugSettingsIntegrationsMarketplaceRouteImport } from "./routes/_app/$orgSlug/settings/integrations/marketplace" +import { Route as AppOrgSlugSettingsIntegrationsInstalledRouteImport } from "./routes/_app/$orgSlug/settings/integrations/installed" +import { Route as AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport } from "./routes/_app/$orgSlug/settings/integrations/$integrationId" +import { Route as AppOrgSlugSettingsChatSyncConnectionIdRouteImport } from "./routes/_app/$orgSlug/settings/chat-sync/$connectionId" +import { Route as AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/layout" +import { Route as AppOrgSlugChatIdFilesIndexRouteImport } from "./routes/_app/$orgSlug/chat/$id/files/index" +import { Route as AppOrgSlugChannelsChannelIdSettingsIndexRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/index" +import { Route as AppOrgSlugChatIdFilesMediaRouteImport } from "./routes/_app/$orgSlug/chat/$id/files/media" +import { Route as AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/overview" +import { Route as AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/integrations" +import { Route as AppOrgSlugChannelsChannelIdSettingsConnectRouteImport } from "./routes/_app/$orgSlug/channels/$channelId/settings/connect" const DevLayoutRoute = DevLayoutRouteImport.update({ - id: '/_dev', - getParentRoute: () => rootRouteImport, + id: "/_dev", + getParentRoute: () => rootRouteImport, } as any) const AppLayoutRoute = AppLayoutRouteImport.update({ - id: '/_app', - getParentRoute: () => rootRouteImport, + id: "/_app", + getParentRoute: () => rootRouteImport, } as any) const AppIndexRoute = AppIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppLayoutRoute, + id: "/", + path: "/", + getParentRoute: () => AppLayoutRoute, } as any) const JoinSlugRoute = JoinSlugRouteImport.update({ - id: '/join/$slug', - path: '/join/$slug', - getParentRoute: () => rootRouteImport, + id: "/join/$slug", + path: "/join/$slug", + getParentRoute: () => rootRouteImport, } as any) const AuthLoginRoute = AuthLoginRouteImport.update({ - id: '/auth/login', - path: '/auth/login', - getParentRoute: () => rootRouteImport, + id: "/auth/login", + path: "/auth/login", + getParentRoute: () => rootRouteImport, } as any) const AuthDesktopLoginRoute = AuthDesktopLoginRouteImport.update({ - id: '/auth/desktop-login', - path: '/auth/desktop-login', - getParentRoute: () => rootRouteImport, + id: "/auth/desktop-login", + path: "/auth/desktop-login", + getParentRoute: () => rootRouteImport, } as any) const AuthDesktopCallbackRoute = AuthDesktopCallbackRouteImport.update({ - id: '/auth/desktop-callback', - path: '/auth/desktop-callback', - getParentRoute: () => rootRouteImport, + id: "/auth/desktop-callback", + path: "/auth/desktop-callback", + getParentRoute: () => rootRouteImport, } as any) const AuthCallbackRoute = AuthCallbackRouteImport.update({ - id: '/auth/callback', - path: '/auth/callback', - getParentRoute: () => rootRouteImport, + id: "/auth/callback", + path: "/auth/callback", + getParentRoute: () => rootRouteImport, } as any) const DevUiLayoutRoute = DevUiLayoutRouteImport.update({ - id: '/ui', - path: '/ui', - getParentRoute: () => DevLayoutRoute, + id: "/ui", + path: "/ui", + getParentRoute: () => DevLayoutRoute, } as any) const AppOrgSlugLayoutRoute = AppOrgSlugLayoutRouteImport.update({ - id: '/$orgSlug', - path: '/$orgSlug', - getParentRoute: () => AppLayoutRoute, + id: "/$orgSlug", + path: "/$orgSlug", + getParentRoute: () => AppLayoutRoute, } as any) const DevEmbedsIndexRoute = DevEmbedsIndexRouteImport.update({ - id: '/dev/embeds/', - path: '/dev/embeds/', - getParentRoute: () => rootRouteImport, + id: "/dev/embeds/", + path: "/dev/embeds/", + getParentRoute: () => rootRouteImport, +} as any) +const AppSelectOrganizationIndexRoute = AppSelectOrganizationIndexRouteImport.update({ + id: "/select-organization/", + path: "/select-organization/", + getParentRoute: () => AppLayoutRoute, } as any) -const AppSelectOrganizationIndexRoute = - AppSelectOrganizationIndexRouteImport.update({ - id: '/select-organization/', - path: '/select-organization/', - getParentRoute: () => AppLayoutRoute, - } as any) const AppOnboardingIndexRoute = AppOnboardingIndexRouteImport.update({ - id: '/onboarding/', - path: '/onboarding/', - getParentRoute: () => AppLayoutRoute, + id: "/onboarding/", + path: "/onboarding/", + getParentRoute: () => AppLayoutRoute, } as any) const AppOrgSlugIndexRoute = AppOrgSlugIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugLayoutRoute, + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugLayoutRoute, } as any) const DevEmbedsRailwayRoute = DevEmbedsRailwayRouteImport.update({ - id: '/dev/embeds/railway', - path: '/dev/embeds/railway', - getParentRoute: () => rootRouteImport, + id: "/dev/embeds/railway", + path: "/dev/embeds/railway", + getParentRoute: () => rootRouteImport, } as any) const DevEmbedsOpenstatusRoute = DevEmbedsOpenstatusRouteImport.update({ - id: '/dev/embeds/openstatus', - path: '/dev/embeds/openstatus', - getParentRoute: () => rootRouteImport, + id: "/dev/embeds/openstatus", + path: "/dev/embeds/openstatus", + getParentRoute: () => rootRouteImport, } as any) const DevEmbedsGithubRoute = DevEmbedsGithubRouteImport.update({ - id: '/dev/embeds/github', - path: '/dev/embeds/github', - getParentRoute: () => rootRouteImport, + id: "/dev/embeds/github", + path: "/dev/embeds/github", + getParentRoute: () => rootRouteImport, } as any) const DevEmbedsDemoRoute = DevEmbedsDemoRouteImport.update({ - id: '/dev/embeds/demo', - path: '/dev/embeds/demo', - getParentRoute: () => rootRouteImport, + id: "/dev/embeds/demo", + path: "/dev/embeds/demo", + getParentRoute: () => rootRouteImport, } as any) const DevUiAgentStepsRoute = DevUiAgentStepsRouteImport.update({ - id: '/agent-steps', - path: '/agent-steps', - getParentRoute: () => DevUiLayoutRoute, + id: "/agent-steps", + path: "/agent-steps", + getParentRoute: () => DevUiLayoutRoute, +} as any) +const AppOnboardingSetupOrganizationRoute = AppOnboardingSetupOrganizationRouteImport.update({ + id: "/onboarding/setup-organization", + path: "/onboarding/setup-organization", + getParentRoute: () => AppLayoutRoute, +} as any) +const AppOrgSlugSettingsLayoutRoute = AppOrgSlugSettingsLayoutRouteImport.update({ + id: "/settings", + path: "/settings", + getParentRoute: () => AppOrgSlugLayoutRoute, +} as any) +const AppOrgSlugNotificationsLayoutRoute = AppOrgSlugNotificationsLayoutRouteImport.update({ + id: "/notifications", + path: "/notifications", + getParentRoute: () => AppOrgSlugLayoutRoute, +} as any) +const AppOrgSlugMySettingsLayoutRoute = AppOrgSlugMySettingsLayoutRouteImport.update({ + id: "/my-settings", + path: "/my-settings", + getParentRoute: () => AppOrgSlugLayoutRoute, } as any) -const AppOnboardingSetupOrganizationRoute = - AppOnboardingSetupOrganizationRouteImport.update({ - id: '/onboarding/setup-organization', - path: '/onboarding/setup-organization', - getParentRoute: () => AppLayoutRoute, - } as any) -const AppOrgSlugSettingsLayoutRoute = - AppOrgSlugSettingsLayoutRouteImport.update({ - id: '/settings', - path: '/settings', - getParentRoute: () => AppOrgSlugLayoutRoute, - } as any) -const AppOrgSlugNotificationsLayoutRoute = - AppOrgSlugNotificationsLayoutRouteImport.update({ - id: '/notifications', - path: '/notifications', - getParentRoute: () => AppOrgSlugLayoutRoute, - } as any) -const AppOrgSlugMySettingsLayoutRoute = - AppOrgSlugMySettingsLayoutRouteImport.update({ - id: '/my-settings', - path: '/my-settings', - getParentRoute: () => AppOrgSlugLayoutRoute, - } as any) const AppOrgSlugSettingsIndexRoute = AppOrgSlugSettingsIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugNotificationsIndexRoute = AppOrgSlugNotificationsIndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, +} as any) +const AppOrgSlugMySettingsIndexRoute = AppOrgSlugMySettingsIndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, } as any) -const AppOrgSlugNotificationsIndexRoute = - AppOrgSlugNotificationsIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, - } as any) -const AppOrgSlugMySettingsIndexRoute = - AppOrgSlugMySettingsIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, - } as any) const AppOrgSlugChatIndexRoute = AppOrgSlugChatIndexRouteImport.update({ - id: '/chat/', - path: '/chat/', - getParentRoute: () => AppOrgSlugLayoutRoute, + id: "/chat/", + path: "/chat/", + getParentRoute: () => AppOrgSlugLayoutRoute, } as any) const AppOrgSlugSettingsTeamRoute = AppOrgSlugSettingsTeamRouteImport.update({ - id: '/team', - path: '/team', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + id: "/team", + path: "/team", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugSettingsInvitationsRoute = AppOrgSlugSettingsInvitationsRouteImport.update({ + id: "/invitations", + path: "/invitations", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, } as any) -const AppOrgSlugSettingsInvitationsRoute = - AppOrgSlugSettingsInvitationsRouteImport.update({ - id: '/invitations', - path: '/invitations', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, - } as any) const AppOrgSlugSettingsDebugRoute = AppOrgSlugSettingsDebugRouteImport.update({ - id: '/debug', - path: '/debug', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, + id: "/debug", + path: "/debug", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugSettingsCustomEmojisRoute = AppOrgSlugSettingsCustomEmojisRouteImport.update({ + id: "/custom-emojis", + path: "/custom-emojis", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugSettingsConnectInvitesRoute = AppOrgSlugSettingsConnectInvitesRouteImport.update({ + id: "/connect-invites", + path: "/connect-invites", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugSettingsAuthenticationRoute = AppOrgSlugSettingsAuthenticationRouteImport.update({ + id: "/authentication", + path: "/authentication", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, } as any) -const AppOrgSlugSettingsCustomEmojisRoute = - AppOrgSlugSettingsCustomEmojisRouteImport.update({ - id: '/custom-emojis', - path: '/custom-emojis', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, - } as any) -const AppOrgSlugSettingsConnectInvitesRoute = - AppOrgSlugSettingsConnectInvitesRouteImport.update({ - id: '/connect-invites', - path: '/connect-invites', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, - } as any) -const AppOrgSlugSettingsAuthenticationRoute = - AppOrgSlugSettingsAuthenticationRouteImport.update({ - id: '/authentication', - path: '/authentication', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, - } as any) const AppOrgSlugProfileUserIdRoute = AppOrgSlugProfileUserIdRouteImport.update({ - id: '/profile/$userId', - path: '/profile/$userId', - getParentRoute: () => AppOrgSlugLayoutRoute, + id: "/profile/$userId", + path: "/profile/$userId", + getParentRoute: () => AppOrgSlugLayoutRoute, +} as any) +const AppOrgSlugNotificationsThreadsRoute = AppOrgSlugNotificationsThreadsRouteImport.update({ + id: "/threads", + path: "/threads", + getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, +} as any) +const AppOrgSlugNotificationsGeneralRoute = AppOrgSlugNotificationsGeneralRouteImport.update({ + id: "/general", + path: "/general", + getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, +} as any) +const AppOrgSlugNotificationsDmsRoute = AppOrgSlugNotificationsDmsRouteImport.update({ + id: "/dms", + path: "/dms", + getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, +} as any) +const AppOrgSlugMySettingsProfileRoute = AppOrgSlugMySettingsProfileRouteImport.update({ + id: "/profile", + path: "/profile", + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, +} as any) +const AppOrgSlugMySettingsNotificationsRoute = AppOrgSlugMySettingsNotificationsRouteImport.update({ + id: "/notifications", + path: "/notifications", + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, +} as any) +const AppOrgSlugMySettingsLinkedAccountsRoute = AppOrgSlugMySettingsLinkedAccountsRouteImport.update({ + id: "/linked-accounts", + path: "/linked-accounts", + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, +} as any) +const AppOrgSlugMySettingsDesktopRoute = AppOrgSlugMySettingsDesktopRouteImport.update({ + id: "/desktop", + path: "/desktop", + getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, } as any) -const AppOrgSlugNotificationsThreadsRoute = - AppOrgSlugNotificationsThreadsRouteImport.update({ - id: '/threads', - path: '/threads', - getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, - } as any) -const AppOrgSlugNotificationsGeneralRoute = - AppOrgSlugNotificationsGeneralRouteImport.update({ - id: '/general', - path: '/general', - getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, - } as any) -const AppOrgSlugNotificationsDmsRoute = - AppOrgSlugNotificationsDmsRouteImport.update({ - id: '/dms', - path: '/dms', - getParentRoute: () => AppOrgSlugNotificationsLayoutRoute, - } as any) -const AppOrgSlugMySettingsProfileRoute = - AppOrgSlugMySettingsProfileRouteImport.update({ - id: '/profile', - path: '/profile', - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, - } as any) -const AppOrgSlugMySettingsNotificationsRoute = - AppOrgSlugMySettingsNotificationsRouteImport.update({ - id: '/notifications', - path: '/notifications', - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, - } as any) -const AppOrgSlugMySettingsLinkedAccountsRoute = - AppOrgSlugMySettingsLinkedAccountsRouteImport.update({ - id: '/linked-accounts', - path: '/linked-accounts', - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, - } as any) -const AppOrgSlugMySettingsDesktopRoute = - AppOrgSlugMySettingsDesktopRouteImport.update({ - id: '/desktop', - path: '/desktop', - getParentRoute: () => AppOrgSlugMySettingsLayoutRoute, - } as any) const AppOrgSlugChatIdRoute = AppOrgSlugChatIdRouteImport.update({ - id: '/chat/$id', - path: '/chat/$id', - getParentRoute: () => AppOrgSlugLayoutRoute, + id: "/chat/$id", + path: "/chat/$id", + getParentRoute: () => AppOrgSlugLayoutRoute, +} as any) +const AppOrgSlugSettingsIntegrationsLayoutRoute = AppOrgSlugSettingsIntegrationsLayoutRouteImport.update({ + id: "/integrations", + path: "/integrations", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugSettingsChatSyncLayoutRoute = AppOrgSlugSettingsChatSyncLayoutRouteImport.update({ + id: "/chat-sync", + path: "/chat-sync", + getParentRoute: () => AppOrgSlugSettingsLayoutRoute, +} as any) +const AppOrgSlugSettingsIntegrationsIndexRoute = AppOrgSlugSettingsIntegrationsIndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, +} as any) +const AppOrgSlugSettingsChatSyncIndexRoute = AppOrgSlugSettingsChatSyncIndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugSettingsChatSyncLayoutRoute, } as any) -const AppOrgSlugSettingsIntegrationsLayoutRoute = - AppOrgSlugSettingsIntegrationsLayoutRouteImport.update({ - id: '/integrations', - path: '/integrations', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, - } as any) -const AppOrgSlugSettingsChatSyncLayoutRoute = - AppOrgSlugSettingsChatSyncLayoutRouteImport.update({ - id: '/chat-sync', - path: '/chat-sync', - getParentRoute: () => AppOrgSlugSettingsLayoutRoute, - } as any) -const AppOrgSlugSettingsIntegrationsIndexRoute = - AppOrgSlugSettingsIntegrationsIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) -const AppOrgSlugSettingsChatSyncIndexRoute = - AppOrgSlugSettingsChatSyncIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugSettingsChatSyncLayoutRoute, - } as any) const AppOrgSlugChatIdIndexRoute = AppOrgSlugChatIdIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugChatIdRoute, + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugChatIdRoute, +} as any) +const AppOrgSlugSettingsIntegrationsYourAppsRoute = AppOrgSlugSettingsIntegrationsYourAppsRouteImport.update({ + id: "/your-apps", + path: "/your-apps", + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, } as any) -const AppOrgSlugSettingsIntegrationsYourAppsRoute = - AppOrgSlugSettingsIntegrationsYourAppsRouteImport.update({ - id: '/your-apps', - path: '/your-apps', - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) const AppOrgSlugSettingsIntegrationsMarketplaceRoute = - AppOrgSlugSettingsIntegrationsMarketplaceRouteImport.update({ - id: '/marketplace', - path: '/marketplace', - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) + AppOrgSlugSettingsIntegrationsMarketplaceRouteImport.update({ + id: "/marketplace", + path: "/marketplace", + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, + } as any) const AppOrgSlugSettingsIntegrationsInstalledRoute = - AppOrgSlugSettingsIntegrationsInstalledRouteImport.update({ - id: '/installed', - path: '/installed', - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) + AppOrgSlugSettingsIntegrationsInstalledRouteImport.update({ + id: "/installed", + path: "/installed", + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, + } as any) const AppOrgSlugSettingsIntegrationsIntegrationIdRoute = - AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport.update({ - id: '/$integrationId', - path: '/$integrationId', - getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, - } as any) -const AppOrgSlugSettingsChatSyncConnectionIdRoute = - AppOrgSlugSettingsChatSyncConnectionIdRouteImport.update({ - id: '/$connectionId', - path: '/$connectionId', - getParentRoute: () => AppOrgSlugSettingsChatSyncLayoutRoute, - } as any) + AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport.update({ + id: "/$integrationId", + path: "/$integrationId", + getParentRoute: () => AppOrgSlugSettingsIntegrationsLayoutRoute, + } as any) +const AppOrgSlugSettingsChatSyncConnectionIdRoute = AppOrgSlugSettingsChatSyncConnectionIdRouteImport.update({ + id: "/$connectionId", + path: "/$connectionId", + getParentRoute: () => AppOrgSlugSettingsChatSyncLayoutRoute, +} as any) const AppOrgSlugChannelsChannelIdSettingsLayoutRoute = - AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport.update({ - id: '/channels/$channelId/settings', - path: '/channels/$channelId/settings', - getParentRoute: () => AppOrgSlugLayoutRoute, - } as any) -const AppOrgSlugChatIdFilesIndexRoute = - AppOrgSlugChatIdFilesIndexRouteImport.update({ - id: '/files/', - path: '/files/', - getParentRoute: () => AppOrgSlugChatIdRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport.update({ + id: "/channels/$channelId/settings", + path: "/channels/$channelId/settings", + getParentRoute: () => AppOrgSlugLayoutRoute, + } as any) +const AppOrgSlugChatIdFilesIndexRoute = AppOrgSlugChatIdFilesIndexRouteImport.update({ + id: "/files/", + path: "/files/", + getParentRoute: () => AppOrgSlugChatIdRoute, +} as any) const AppOrgSlugChannelsChannelIdSettingsIndexRoute = - AppOrgSlugChannelsChannelIdSettingsIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, - } as any) -const AppOrgSlugChatIdFilesMediaRoute = - AppOrgSlugChatIdFilesMediaRouteImport.update({ - id: '/files/media', - path: '/files/media', - getParentRoute: () => AppOrgSlugChatIdRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsIndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, + } as any) +const AppOrgSlugChatIdFilesMediaRoute = AppOrgSlugChatIdFilesMediaRouteImport.update({ + id: "/files/media", + path: "/files/media", + getParentRoute: () => AppOrgSlugChatIdRoute, +} as any) const AppOrgSlugChannelsChannelIdSettingsOverviewRoute = - AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport.update({ - id: '/overview', - path: '/overview', - getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport.update({ + id: "/overview", + path: "/overview", + getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, + } as any) const AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute = - AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport.update({ - id: '/integrations', - path: '/integrations', - getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport.update({ + id: "/integrations", + path: "/integrations", + getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, + } as any) const AppOrgSlugChannelsChannelIdSettingsConnectRoute = - AppOrgSlugChannelsChannelIdSettingsConnectRouteImport.update({ - id: '/connect', - path: '/connect', - getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, - } as any) + AppOrgSlugChannelsChannelIdSettingsConnectRouteImport.update({ + id: "/connect", + path: "/connect", + getParentRoute: () => AppOrgSlugChannelsChannelIdSettingsLayoutRoute, + } as any) export interface FileRoutesByFullPath { - '/': typeof AppIndexRoute - '/$orgSlug': typeof AppOrgSlugLayoutRouteWithChildren - '/ui': typeof DevUiLayoutRouteWithChildren - '/auth/callback': typeof AuthCallbackRoute - '/auth/desktop-callback': typeof AuthDesktopCallbackRoute - '/auth/desktop-login': typeof AuthDesktopLoginRoute - '/auth/login': typeof AuthLoginRoute - '/join/$slug': typeof JoinSlugRoute - '/$orgSlug/my-settings': typeof AppOrgSlugMySettingsLayoutRouteWithChildren - '/$orgSlug/notifications': typeof AppOrgSlugNotificationsLayoutRouteWithChildren - '/$orgSlug/settings': typeof AppOrgSlugSettingsLayoutRouteWithChildren - '/onboarding/setup-organization': typeof AppOnboardingSetupOrganizationRoute - '/ui/agent-steps': typeof DevUiAgentStepsRoute - '/dev/embeds/demo': typeof DevEmbedsDemoRoute - '/dev/embeds/github': typeof DevEmbedsGithubRoute - '/dev/embeds/openstatus': typeof DevEmbedsOpenstatusRoute - '/dev/embeds/railway': typeof DevEmbedsRailwayRoute - '/$orgSlug/': typeof AppOrgSlugIndexRoute - '/onboarding/': typeof AppOnboardingIndexRoute - '/select-organization/': typeof AppSelectOrganizationIndexRoute - '/dev/embeds/': typeof DevEmbedsIndexRoute - '/$orgSlug/settings/chat-sync': typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren - '/$orgSlug/settings/integrations': typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren - '/$orgSlug/chat/$id': typeof AppOrgSlugChatIdRouteWithChildren - '/$orgSlug/my-settings/desktop': typeof AppOrgSlugMySettingsDesktopRoute - '/$orgSlug/my-settings/linked-accounts': typeof AppOrgSlugMySettingsLinkedAccountsRoute - '/$orgSlug/my-settings/notifications': typeof AppOrgSlugMySettingsNotificationsRoute - '/$orgSlug/my-settings/profile': typeof AppOrgSlugMySettingsProfileRoute - '/$orgSlug/notifications/dms': typeof AppOrgSlugNotificationsDmsRoute - '/$orgSlug/notifications/general': typeof AppOrgSlugNotificationsGeneralRoute - '/$orgSlug/notifications/threads': typeof AppOrgSlugNotificationsThreadsRoute - '/$orgSlug/profile/$userId': typeof AppOrgSlugProfileUserIdRoute - '/$orgSlug/settings/authentication': typeof AppOrgSlugSettingsAuthenticationRoute - '/$orgSlug/settings/connect-invites': typeof AppOrgSlugSettingsConnectInvitesRoute - '/$orgSlug/settings/custom-emojis': typeof AppOrgSlugSettingsCustomEmojisRoute - '/$orgSlug/settings/debug': typeof AppOrgSlugSettingsDebugRoute - '/$orgSlug/settings/invitations': typeof AppOrgSlugSettingsInvitationsRoute - '/$orgSlug/settings/team': typeof AppOrgSlugSettingsTeamRoute - '/$orgSlug/chat/': typeof AppOrgSlugChatIndexRoute - '/$orgSlug/my-settings/': typeof AppOrgSlugMySettingsIndexRoute - '/$orgSlug/notifications/': typeof AppOrgSlugNotificationsIndexRoute - '/$orgSlug/settings/': typeof AppOrgSlugSettingsIndexRoute - '/$orgSlug/channels/$channelId/settings': typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren - '/$orgSlug/settings/chat-sync/$connectionId': typeof AppOrgSlugSettingsChatSyncConnectionIdRoute - '/$orgSlug/settings/integrations/$integrationId': typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute - '/$orgSlug/settings/integrations/installed': typeof AppOrgSlugSettingsIntegrationsInstalledRoute - '/$orgSlug/settings/integrations/marketplace': typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute - '/$orgSlug/settings/integrations/your-apps': typeof AppOrgSlugSettingsIntegrationsYourAppsRoute - '/$orgSlug/chat/$id/': typeof AppOrgSlugChatIdIndexRoute - '/$orgSlug/settings/chat-sync/': typeof AppOrgSlugSettingsChatSyncIndexRoute - '/$orgSlug/settings/integrations/': typeof AppOrgSlugSettingsIntegrationsIndexRoute - '/$orgSlug/channels/$channelId/settings/connect': typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute - '/$orgSlug/channels/$channelId/settings/integrations': typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute - '/$orgSlug/channels/$channelId/settings/overview': typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute - '/$orgSlug/chat/$id/files/media': typeof AppOrgSlugChatIdFilesMediaRoute - '/$orgSlug/channels/$channelId/settings/': typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute - '/$orgSlug/chat/$id/files/': typeof AppOrgSlugChatIdFilesIndexRoute + "/": typeof AppIndexRoute + "/$orgSlug": typeof AppOrgSlugLayoutRouteWithChildren + "/ui": typeof DevUiLayoutRouteWithChildren + "/auth/callback": typeof AuthCallbackRoute + "/auth/desktop-callback": typeof AuthDesktopCallbackRoute + "/auth/desktop-login": typeof AuthDesktopLoginRoute + "/auth/login": typeof AuthLoginRoute + "/join/$slug": typeof JoinSlugRoute + "/$orgSlug/my-settings": typeof AppOrgSlugMySettingsLayoutRouteWithChildren + "/$orgSlug/notifications": typeof AppOrgSlugNotificationsLayoutRouteWithChildren + "/$orgSlug/settings": typeof AppOrgSlugSettingsLayoutRouteWithChildren + "/onboarding/setup-organization": typeof AppOnboardingSetupOrganizationRoute + "/ui/agent-steps": typeof DevUiAgentStepsRoute + "/dev/embeds/demo": typeof DevEmbedsDemoRoute + "/dev/embeds/github": typeof DevEmbedsGithubRoute + "/dev/embeds/openstatus": typeof DevEmbedsOpenstatusRoute + "/dev/embeds/railway": typeof DevEmbedsRailwayRoute + "/$orgSlug/": typeof AppOrgSlugIndexRoute + "/onboarding/": typeof AppOnboardingIndexRoute + "/select-organization/": typeof AppSelectOrganizationIndexRoute + "/dev/embeds/": typeof DevEmbedsIndexRoute + "/$orgSlug/settings/chat-sync": typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren + "/$orgSlug/settings/integrations": typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren + "/$orgSlug/chat/$id": typeof AppOrgSlugChatIdRouteWithChildren + "/$orgSlug/my-settings/desktop": typeof AppOrgSlugMySettingsDesktopRoute + "/$orgSlug/my-settings/linked-accounts": typeof AppOrgSlugMySettingsLinkedAccountsRoute + "/$orgSlug/my-settings/notifications": typeof AppOrgSlugMySettingsNotificationsRoute + "/$orgSlug/my-settings/profile": typeof AppOrgSlugMySettingsProfileRoute + "/$orgSlug/notifications/dms": typeof AppOrgSlugNotificationsDmsRoute + "/$orgSlug/notifications/general": typeof AppOrgSlugNotificationsGeneralRoute + "/$orgSlug/notifications/threads": typeof AppOrgSlugNotificationsThreadsRoute + "/$orgSlug/profile/$userId": typeof AppOrgSlugProfileUserIdRoute + "/$orgSlug/settings/authentication": typeof AppOrgSlugSettingsAuthenticationRoute + "/$orgSlug/settings/connect-invites": typeof AppOrgSlugSettingsConnectInvitesRoute + "/$orgSlug/settings/custom-emojis": typeof AppOrgSlugSettingsCustomEmojisRoute + "/$orgSlug/settings/debug": typeof AppOrgSlugSettingsDebugRoute + "/$orgSlug/settings/invitations": typeof AppOrgSlugSettingsInvitationsRoute + "/$orgSlug/settings/team": typeof AppOrgSlugSettingsTeamRoute + "/$orgSlug/chat/": typeof AppOrgSlugChatIndexRoute + "/$orgSlug/my-settings/": typeof AppOrgSlugMySettingsIndexRoute + "/$orgSlug/notifications/": typeof AppOrgSlugNotificationsIndexRoute + "/$orgSlug/settings/": typeof AppOrgSlugSettingsIndexRoute + "/$orgSlug/channels/$channelId/settings": typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren + "/$orgSlug/settings/chat-sync/$connectionId": typeof AppOrgSlugSettingsChatSyncConnectionIdRoute + "/$orgSlug/settings/integrations/$integrationId": typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute + "/$orgSlug/settings/integrations/installed": typeof AppOrgSlugSettingsIntegrationsInstalledRoute + "/$orgSlug/settings/integrations/marketplace": typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute + "/$orgSlug/settings/integrations/your-apps": typeof AppOrgSlugSettingsIntegrationsYourAppsRoute + "/$orgSlug/chat/$id/": typeof AppOrgSlugChatIdIndexRoute + "/$orgSlug/settings/chat-sync/": typeof AppOrgSlugSettingsChatSyncIndexRoute + "/$orgSlug/settings/integrations/": typeof AppOrgSlugSettingsIntegrationsIndexRoute + "/$orgSlug/channels/$channelId/settings/connect": typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute + "/$orgSlug/channels/$channelId/settings/integrations": typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute + "/$orgSlug/channels/$channelId/settings/overview": typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute + "/$orgSlug/chat/$id/files/media": typeof AppOrgSlugChatIdFilesMediaRoute + "/$orgSlug/channels/$channelId/settings/": typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute + "/$orgSlug/chat/$id/files/": typeof AppOrgSlugChatIdFilesIndexRoute } export interface FileRoutesByTo { - '/': typeof AppIndexRoute - '/ui': typeof DevUiLayoutRouteWithChildren - '/auth/callback': typeof AuthCallbackRoute - '/auth/desktop-callback': typeof AuthDesktopCallbackRoute - '/auth/desktop-login': typeof AuthDesktopLoginRoute - '/auth/login': typeof AuthLoginRoute - '/join/$slug': typeof JoinSlugRoute - '/onboarding/setup-organization': typeof AppOnboardingSetupOrganizationRoute - '/ui/agent-steps': typeof DevUiAgentStepsRoute - '/dev/embeds/demo': typeof DevEmbedsDemoRoute - '/dev/embeds/github': typeof DevEmbedsGithubRoute - '/dev/embeds/openstatus': typeof DevEmbedsOpenstatusRoute - '/dev/embeds/railway': typeof DevEmbedsRailwayRoute - '/$orgSlug': typeof AppOrgSlugIndexRoute - '/onboarding': typeof AppOnboardingIndexRoute - '/select-organization': typeof AppSelectOrganizationIndexRoute - '/dev/embeds': typeof DevEmbedsIndexRoute - '/$orgSlug/my-settings/desktop': typeof AppOrgSlugMySettingsDesktopRoute - '/$orgSlug/my-settings/linked-accounts': typeof AppOrgSlugMySettingsLinkedAccountsRoute - '/$orgSlug/my-settings/notifications': typeof AppOrgSlugMySettingsNotificationsRoute - '/$orgSlug/my-settings/profile': typeof AppOrgSlugMySettingsProfileRoute - '/$orgSlug/notifications/dms': typeof AppOrgSlugNotificationsDmsRoute - '/$orgSlug/notifications/general': typeof AppOrgSlugNotificationsGeneralRoute - '/$orgSlug/notifications/threads': typeof AppOrgSlugNotificationsThreadsRoute - '/$orgSlug/profile/$userId': typeof AppOrgSlugProfileUserIdRoute - '/$orgSlug/settings/authentication': typeof AppOrgSlugSettingsAuthenticationRoute - '/$orgSlug/settings/connect-invites': typeof AppOrgSlugSettingsConnectInvitesRoute - '/$orgSlug/settings/custom-emojis': typeof AppOrgSlugSettingsCustomEmojisRoute - '/$orgSlug/settings/debug': typeof AppOrgSlugSettingsDebugRoute - '/$orgSlug/settings/invitations': typeof AppOrgSlugSettingsInvitationsRoute - '/$orgSlug/settings/team': typeof AppOrgSlugSettingsTeamRoute - '/$orgSlug/chat': typeof AppOrgSlugChatIndexRoute - '/$orgSlug/my-settings': typeof AppOrgSlugMySettingsIndexRoute - '/$orgSlug/notifications': typeof AppOrgSlugNotificationsIndexRoute - '/$orgSlug/settings': typeof AppOrgSlugSettingsIndexRoute - '/$orgSlug/settings/chat-sync/$connectionId': typeof AppOrgSlugSettingsChatSyncConnectionIdRoute - '/$orgSlug/settings/integrations/$integrationId': typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute - '/$orgSlug/settings/integrations/installed': typeof AppOrgSlugSettingsIntegrationsInstalledRoute - '/$orgSlug/settings/integrations/marketplace': typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute - '/$orgSlug/settings/integrations/your-apps': typeof AppOrgSlugSettingsIntegrationsYourAppsRoute - '/$orgSlug/chat/$id': typeof AppOrgSlugChatIdIndexRoute - '/$orgSlug/settings/chat-sync': typeof AppOrgSlugSettingsChatSyncIndexRoute - '/$orgSlug/settings/integrations': typeof AppOrgSlugSettingsIntegrationsIndexRoute - '/$orgSlug/channels/$channelId/settings/connect': typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute - '/$orgSlug/channels/$channelId/settings/integrations': typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute - '/$orgSlug/channels/$channelId/settings/overview': typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute - '/$orgSlug/chat/$id/files/media': typeof AppOrgSlugChatIdFilesMediaRoute - '/$orgSlug/channels/$channelId/settings': typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute - '/$orgSlug/chat/$id/files': typeof AppOrgSlugChatIdFilesIndexRoute + "/": typeof AppIndexRoute + "/ui": typeof DevUiLayoutRouteWithChildren + "/auth/callback": typeof AuthCallbackRoute + "/auth/desktop-callback": typeof AuthDesktopCallbackRoute + "/auth/desktop-login": typeof AuthDesktopLoginRoute + "/auth/login": typeof AuthLoginRoute + "/join/$slug": typeof JoinSlugRoute + "/onboarding/setup-organization": typeof AppOnboardingSetupOrganizationRoute + "/ui/agent-steps": typeof DevUiAgentStepsRoute + "/dev/embeds/demo": typeof DevEmbedsDemoRoute + "/dev/embeds/github": typeof DevEmbedsGithubRoute + "/dev/embeds/openstatus": typeof DevEmbedsOpenstatusRoute + "/dev/embeds/railway": typeof DevEmbedsRailwayRoute + "/$orgSlug": typeof AppOrgSlugIndexRoute + "/onboarding": typeof AppOnboardingIndexRoute + "/select-organization": typeof AppSelectOrganizationIndexRoute + "/dev/embeds": typeof DevEmbedsIndexRoute + "/$orgSlug/my-settings/desktop": typeof AppOrgSlugMySettingsDesktopRoute + "/$orgSlug/my-settings/linked-accounts": typeof AppOrgSlugMySettingsLinkedAccountsRoute + "/$orgSlug/my-settings/notifications": typeof AppOrgSlugMySettingsNotificationsRoute + "/$orgSlug/my-settings/profile": typeof AppOrgSlugMySettingsProfileRoute + "/$orgSlug/notifications/dms": typeof AppOrgSlugNotificationsDmsRoute + "/$orgSlug/notifications/general": typeof AppOrgSlugNotificationsGeneralRoute + "/$orgSlug/notifications/threads": typeof AppOrgSlugNotificationsThreadsRoute + "/$orgSlug/profile/$userId": typeof AppOrgSlugProfileUserIdRoute + "/$orgSlug/settings/authentication": typeof AppOrgSlugSettingsAuthenticationRoute + "/$orgSlug/settings/connect-invites": typeof AppOrgSlugSettingsConnectInvitesRoute + "/$orgSlug/settings/custom-emojis": typeof AppOrgSlugSettingsCustomEmojisRoute + "/$orgSlug/settings/debug": typeof AppOrgSlugSettingsDebugRoute + "/$orgSlug/settings/invitations": typeof AppOrgSlugSettingsInvitationsRoute + "/$orgSlug/settings/team": typeof AppOrgSlugSettingsTeamRoute + "/$orgSlug/chat": typeof AppOrgSlugChatIndexRoute + "/$orgSlug/my-settings": typeof AppOrgSlugMySettingsIndexRoute + "/$orgSlug/notifications": typeof AppOrgSlugNotificationsIndexRoute + "/$orgSlug/settings": typeof AppOrgSlugSettingsIndexRoute + "/$orgSlug/settings/chat-sync/$connectionId": typeof AppOrgSlugSettingsChatSyncConnectionIdRoute + "/$orgSlug/settings/integrations/$integrationId": typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute + "/$orgSlug/settings/integrations/installed": typeof AppOrgSlugSettingsIntegrationsInstalledRoute + "/$orgSlug/settings/integrations/marketplace": typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute + "/$orgSlug/settings/integrations/your-apps": typeof AppOrgSlugSettingsIntegrationsYourAppsRoute + "/$orgSlug/chat/$id": typeof AppOrgSlugChatIdIndexRoute + "/$orgSlug/settings/chat-sync": typeof AppOrgSlugSettingsChatSyncIndexRoute + "/$orgSlug/settings/integrations": typeof AppOrgSlugSettingsIntegrationsIndexRoute + "/$orgSlug/channels/$channelId/settings/connect": typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute + "/$orgSlug/channels/$channelId/settings/integrations": typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute + "/$orgSlug/channels/$channelId/settings/overview": typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute + "/$orgSlug/chat/$id/files/media": typeof AppOrgSlugChatIdFilesMediaRoute + "/$orgSlug/channels/$channelId/settings": typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute + "/$orgSlug/chat/$id/files": typeof AppOrgSlugChatIdFilesIndexRoute } export interface FileRoutesById { - __root__: typeof rootRouteImport - '/_app': typeof AppLayoutRouteWithChildren - '/_dev': typeof DevLayoutRouteWithChildren - '/_app/$orgSlug': typeof AppOrgSlugLayoutRouteWithChildren - '/_dev/ui': typeof DevUiLayoutRouteWithChildren - '/auth/callback': typeof AuthCallbackRoute - '/auth/desktop-callback': typeof AuthDesktopCallbackRoute - '/auth/desktop-login': typeof AuthDesktopLoginRoute - '/auth/login': typeof AuthLoginRoute - '/join/$slug': typeof JoinSlugRoute - '/_app/': typeof AppIndexRoute - '/_app/$orgSlug/my-settings': typeof AppOrgSlugMySettingsLayoutRouteWithChildren - '/_app/$orgSlug/notifications': typeof AppOrgSlugNotificationsLayoutRouteWithChildren - '/_app/$orgSlug/settings': typeof AppOrgSlugSettingsLayoutRouteWithChildren - '/_app/onboarding/setup-organization': typeof AppOnboardingSetupOrganizationRoute - '/_dev/ui/agent-steps': typeof DevUiAgentStepsRoute - '/dev/embeds/demo': typeof DevEmbedsDemoRoute - '/dev/embeds/github': typeof DevEmbedsGithubRoute - '/dev/embeds/openstatus': typeof DevEmbedsOpenstatusRoute - '/dev/embeds/railway': typeof DevEmbedsRailwayRoute - '/_app/$orgSlug/': typeof AppOrgSlugIndexRoute - '/_app/onboarding/': typeof AppOnboardingIndexRoute - '/_app/select-organization/': typeof AppSelectOrganizationIndexRoute - '/dev/embeds/': typeof DevEmbedsIndexRoute - '/_app/$orgSlug/settings/chat-sync': typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren - '/_app/$orgSlug/settings/integrations': typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren - '/_app/$orgSlug/chat/$id': typeof AppOrgSlugChatIdRouteWithChildren - '/_app/$orgSlug/my-settings/desktop': typeof AppOrgSlugMySettingsDesktopRoute - '/_app/$orgSlug/my-settings/linked-accounts': typeof AppOrgSlugMySettingsLinkedAccountsRoute - '/_app/$orgSlug/my-settings/notifications': typeof AppOrgSlugMySettingsNotificationsRoute - '/_app/$orgSlug/my-settings/profile': typeof AppOrgSlugMySettingsProfileRoute - '/_app/$orgSlug/notifications/dms': typeof AppOrgSlugNotificationsDmsRoute - '/_app/$orgSlug/notifications/general': typeof AppOrgSlugNotificationsGeneralRoute - '/_app/$orgSlug/notifications/threads': typeof AppOrgSlugNotificationsThreadsRoute - '/_app/$orgSlug/profile/$userId': typeof AppOrgSlugProfileUserIdRoute - '/_app/$orgSlug/settings/authentication': typeof AppOrgSlugSettingsAuthenticationRoute - '/_app/$orgSlug/settings/connect-invites': typeof AppOrgSlugSettingsConnectInvitesRoute - '/_app/$orgSlug/settings/custom-emojis': typeof AppOrgSlugSettingsCustomEmojisRoute - '/_app/$orgSlug/settings/debug': typeof AppOrgSlugSettingsDebugRoute - '/_app/$orgSlug/settings/invitations': typeof AppOrgSlugSettingsInvitationsRoute - '/_app/$orgSlug/settings/team': typeof AppOrgSlugSettingsTeamRoute - '/_app/$orgSlug/chat/': typeof AppOrgSlugChatIndexRoute - '/_app/$orgSlug/my-settings/': typeof AppOrgSlugMySettingsIndexRoute - '/_app/$orgSlug/notifications/': typeof AppOrgSlugNotificationsIndexRoute - '/_app/$orgSlug/settings/': typeof AppOrgSlugSettingsIndexRoute - '/_app/$orgSlug/channels/$channelId/settings': typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren - '/_app/$orgSlug/settings/chat-sync/$connectionId': typeof AppOrgSlugSettingsChatSyncConnectionIdRoute - '/_app/$orgSlug/settings/integrations/$integrationId': typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute - '/_app/$orgSlug/settings/integrations/installed': typeof AppOrgSlugSettingsIntegrationsInstalledRoute - '/_app/$orgSlug/settings/integrations/marketplace': typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute - '/_app/$orgSlug/settings/integrations/your-apps': typeof AppOrgSlugSettingsIntegrationsYourAppsRoute - '/_app/$orgSlug/chat/$id/': typeof AppOrgSlugChatIdIndexRoute - '/_app/$orgSlug/settings/chat-sync/': typeof AppOrgSlugSettingsChatSyncIndexRoute - '/_app/$orgSlug/settings/integrations/': typeof AppOrgSlugSettingsIntegrationsIndexRoute - '/_app/$orgSlug/channels/$channelId/settings/connect': typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute - '/_app/$orgSlug/channels/$channelId/settings/integrations': typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute - '/_app/$orgSlug/channels/$channelId/settings/overview': typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute - '/_app/$orgSlug/chat/$id/files/media': typeof AppOrgSlugChatIdFilesMediaRoute - '/_app/$orgSlug/channels/$channelId/settings/': typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute - '/_app/$orgSlug/chat/$id/files/': typeof AppOrgSlugChatIdFilesIndexRoute + __root__: typeof rootRouteImport + "/_app": typeof AppLayoutRouteWithChildren + "/_dev": typeof DevLayoutRouteWithChildren + "/_app/$orgSlug": typeof AppOrgSlugLayoutRouteWithChildren + "/_dev/ui": typeof DevUiLayoutRouteWithChildren + "/auth/callback": typeof AuthCallbackRoute + "/auth/desktop-callback": typeof AuthDesktopCallbackRoute + "/auth/desktop-login": typeof AuthDesktopLoginRoute + "/auth/login": typeof AuthLoginRoute + "/join/$slug": typeof JoinSlugRoute + "/_app/": typeof AppIndexRoute + "/_app/$orgSlug/my-settings": typeof AppOrgSlugMySettingsLayoutRouteWithChildren + "/_app/$orgSlug/notifications": typeof AppOrgSlugNotificationsLayoutRouteWithChildren + "/_app/$orgSlug/settings": typeof AppOrgSlugSettingsLayoutRouteWithChildren + "/_app/onboarding/setup-organization": typeof AppOnboardingSetupOrganizationRoute + "/_dev/ui/agent-steps": typeof DevUiAgentStepsRoute + "/dev/embeds/demo": typeof DevEmbedsDemoRoute + "/dev/embeds/github": typeof DevEmbedsGithubRoute + "/dev/embeds/openstatus": typeof DevEmbedsOpenstatusRoute + "/dev/embeds/railway": typeof DevEmbedsRailwayRoute + "/_app/$orgSlug/": typeof AppOrgSlugIndexRoute + "/_app/onboarding/": typeof AppOnboardingIndexRoute + "/_app/select-organization/": typeof AppSelectOrganizationIndexRoute + "/dev/embeds/": typeof DevEmbedsIndexRoute + "/_app/$orgSlug/settings/chat-sync": typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren + "/_app/$orgSlug/settings/integrations": typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren + "/_app/$orgSlug/chat/$id": typeof AppOrgSlugChatIdRouteWithChildren + "/_app/$orgSlug/my-settings/desktop": typeof AppOrgSlugMySettingsDesktopRoute + "/_app/$orgSlug/my-settings/linked-accounts": typeof AppOrgSlugMySettingsLinkedAccountsRoute + "/_app/$orgSlug/my-settings/notifications": typeof AppOrgSlugMySettingsNotificationsRoute + "/_app/$orgSlug/my-settings/profile": typeof AppOrgSlugMySettingsProfileRoute + "/_app/$orgSlug/notifications/dms": typeof AppOrgSlugNotificationsDmsRoute + "/_app/$orgSlug/notifications/general": typeof AppOrgSlugNotificationsGeneralRoute + "/_app/$orgSlug/notifications/threads": typeof AppOrgSlugNotificationsThreadsRoute + "/_app/$orgSlug/profile/$userId": typeof AppOrgSlugProfileUserIdRoute + "/_app/$orgSlug/settings/authentication": typeof AppOrgSlugSettingsAuthenticationRoute + "/_app/$orgSlug/settings/connect-invites": typeof AppOrgSlugSettingsConnectInvitesRoute + "/_app/$orgSlug/settings/custom-emojis": typeof AppOrgSlugSettingsCustomEmojisRoute + "/_app/$orgSlug/settings/debug": typeof AppOrgSlugSettingsDebugRoute + "/_app/$orgSlug/settings/invitations": typeof AppOrgSlugSettingsInvitationsRoute + "/_app/$orgSlug/settings/team": typeof AppOrgSlugSettingsTeamRoute + "/_app/$orgSlug/chat/": typeof AppOrgSlugChatIndexRoute + "/_app/$orgSlug/my-settings/": typeof AppOrgSlugMySettingsIndexRoute + "/_app/$orgSlug/notifications/": typeof AppOrgSlugNotificationsIndexRoute + "/_app/$orgSlug/settings/": typeof AppOrgSlugSettingsIndexRoute + "/_app/$orgSlug/channels/$channelId/settings": typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren + "/_app/$orgSlug/settings/chat-sync/$connectionId": typeof AppOrgSlugSettingsChatSyncConnectionIdRoute + "/_app/$orgSlug/settings/integrations/$integrationId": typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute + "/_app/$orgSlug/settings/integrations/installed": typeof AppOrgSlugSettingsIntegrationsInstalledRoute + "/_app/$orgSlug/settings/integrations/marketplace": typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute + "/_app/$orgSlug/settings/integrations/your-apps": typeof AppOrgSlugSettingsIntegrationsYourAppsRoute + "/_app/$orgSlug/chat/$id/": typeof AppOrgSlugChatIdIndexRoute + "/_app/$orgSlug/settings/chat-sync/": typeof AppOrgSlugSettingsChatSyncIndexRoute + "/_app/$orgSlug/settings/integrations/": typeof AppOrgSlugSettingsIntegrationsIndexRoute + "/_app/$orgSlug/channels/$channelId/settings/connect": typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute + "/_app/$orgSlug/channels/$channelId/settings/integrations": typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute + "/_app/$orgSlug/channels/$channelId/settings/overview": typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute + "/_app/$orgSlug/chat/$id/files/media": typeof AppOrgSlugChatIdFilesMediaRoute + "/_app/$orgSlug/channels/$channelId/settings/": typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute + "/_app/$orgSlug/chat/$id/files/": typeof AppOrgSlugChatIdFilesIndexRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: - | '/' - | '/$orgSlug' - | '/ui' - | '/auth/callback' - | '/auth/desktop-callback' - | '/auth/desktop-login' - | '/auth/login' - | '/join/$slug' - | '/$orgSlug/my-settings' - | '/$orgSlug/notifications' - | '/$orgSlug/settings' - | '/onboarding/setup-organization' - | '/ui/agent-steps' - | '/dev/embeds/demo' - | '/dev/embeds/github' - | '/dev/embeds/openstatus' - | '/dev/embeds/railway' - | '/$orgSlug/' - | '/onboarding/' - | '/select-organization/' - | '/dev/embeds/' - | '/$orgSlug/settings/chat-sync' - | '/$orgSlug/settings/integrations' - | '/$orgSlug/chat/$id' - | '/$orgSlug/my-settings/desktop' - | '/$orgSlug/my-settings/linked-accounts' - | '/$orgSlug/my-settings/notifications' - | '/$orgSlug/my-settings/profile' - | '/$orgSlug/notifications/dms' - | '/$orgSlug/notifications/general' - | '/$orgSlug/notifications/threads' - | '/$orgSlug/profile/$userId' - | '/$orgSlug/settings/authentication' - | '/$orgSlug/settings/connect-invites' - | '/$orgSlug/settings/custom-emojis' - | '/$orgSlug/settings/debug' - | '/$orgSlug/settings/invitations' - | '/$orgSlug/settings/team' - | '/$orgSlug/chat/' - | '/$orgSlug/my-settings/' - | '/$orgSlug/notifications/' - | '/$orgSlug/settings/' - | '/$orgSlug/channels/$channelId/settings' - | '/$orgSlug/settings/chat-sync/$connectionId' - | '/$orgSlug/settings/integrations/$integrationId' - | '/$orgSlug/settings/integrations/installed' - | '/$orgSlug/settings/integrations/marketplace' - | '/$orgSlug/settings/integrations/your-apps' - | '/$orgSlug/chat/$id/' - | '/$orgSlug/settings/chat-sync/' - | '/$orgSlug/settings/integrations/' - | '/$orgSlug/channels/$channelId/settings/connect' - | '/$orgSlug/channels/$channelId/settings/integrations' - | '/$orgSlug/channels/$channelId/settings/overview' - | '/$orgSlug/chat/$id/files/media' - | '/$orgSlug/channels/$channelId/settings/' - | '/$orgSlug/chat/$id/files/' - fileRoutesByTo: FileRoutesByTo - to: - | '/' - | '/ui' - | '/auth/callback' - | '/auth/desktop-callback' - | '/auth/desktop-login' - | '/auth/login' - | '/join/$slug' - | '/onboarding/setup-organization' - | '/ui/agent-steps' - | '/dev/embeds/demo' - | '/dev/embeds/github' - | '/dev/embeds/openstatus' - | '/dev/embeds/railway' - | '/$orgSlug' - | '/onboarding' - | '/select-organization' - | '/dev/embeds' - | '/$orgSlug/my-settings/desktop' - | '/$orgSlug/my-settings/linked-accounts' - | '/$orgSlug/my-settings/notifications' - | '/$orgSlug/my-settings/profile' - | '/$orgSlug/notifications/dms' - | '/$orgSlug/notifications/general' - | '/$orgSlug/notifications/threads' - | '/$orgSlug/profile/$userId' - | '/$orgSlug/settings/authentication' - | '/$orgSlug/settings/connect-invites' - | '/$orgSlug/settings/custom-emojis' - | '/$orgSlug/settings/debug' - | '/$orgSlug/settings/invitations' - | '/$orgSlug/settings/team' - | '/$orgSlug/chat' - | '/$orgSlug/my-settings' - | '/$orgSlug/notifications' - | '/$orgSlug/settings' - | '/$orgSlug/settings/chat-sync/$connectionId' - | '/$orgSlug/settings/integrations/$integrationId' - | '/$orgSlug/settings/integrations/installed' - | '/$orgSlug/settings/integrations/marketplace' - | '/$orgSlug/settings/integrations/your-apps' - | '/$orgSlug/chat/$id' - | '/$orgSlug/settings/chat-sync' - | '/$orgSlug/settings/integrations' - | '/$orgSlug/channels/$channelId/settings/connect' - | '/$orgSlug/channels/$channelId/settings/integrations' - | '/$orgSlug/channels/$channelId/settings/overview' - | '/$orgSlug/chat/$id/files/media' - | '/$orgSlug/channels/$channelId/settings' - | '/$orgSlug/chat/$id/files' - id: - | '__root__' - | '/_app' - | '/_dev' - | '/_app/$orgSlug' - | '/_dev/ui' - | '/auth/callback' - | '/auth/desktop-callback' - | '/auth/desktop-login' - | '/auth/login' - | '/join/$slug' - | '/_app/' - | '/_app/$orgSlug/my-settings' - | '/_app/$orgSlug/notifications' - | '/_app/$orgSlug/settings' - | '/_app/onboarding/setup-organization' - | '/_dev/ui/agent-steps' - | '/dev/embeds/demo' - | '/dev/embeds/github' - | '/dev/embeds/openstatus' - | '/dev/embeds/railway' - | '/_app/$orgSlug/' - | '/_app/onboarding/' - | '/_app/select-organization/' - | '/dev/embeds/' - | '/_app/$orgSlug/settings/chat-sync' - | '/_app/$orgSlug/settings/integrations' - | '/_app/$orgSlug/chat/$id' - | '/_app/$orgSlug/my-settings/desktop' - | '/_app/$orgSlug/my-settings/linked-accounts' - | '/_app/$orgSlug/my-settings/notifications' - | '/_app/$orgSlug/my-settings/profile' - | '/_app/$orgSlug/notifications/dms' - | '/_app/$orgSlug/notifications/general' - | '/_app/$orgSlug/notifications/threads' - | '/_app/$orgSlug/profile/$userId' - | '/_app/$orgSlug/settings/authentication' - | '/_app/$orgSlug/settings/connect-invites' - | '/_app/$orgSlug/settings/custom-emojis' - | '/_app/$orgSlug/settings/debug' - | '/_app/$orgSlug/settings/invitations' - | '/_app/$orgSlug/settings/team' - | '/_app/$orgSlug/chat/' - | '/_app/$orgSlug/my-settings/' - | '/_app/$orgSlug/notifications/' - | '/_app/$orgSlug/settings/' - | '/_app/$orgSlug/channels/$channelId/settings' - | '/_app/$orgSlug/settings/chat-sync/$connectionId' - | '/_app/$orgSlug/settings/integrations/$integrationId' - | '/_app/$orgSlug/settings/integrations/installed' - | '/_app/$orgSlug/settings/integrations/marketplace' - | '/_app/$orgSlug/settings/integrations/your-apps' - | '/_app/$orgSlug/chat/$id/' - | '/_app/$orgSlug/settings/chat-sync/' - | '/_app/$orgSlug/settings/integrations/' - | '/_app/$orgSlug/channels/$channelId/settings/connect' - | '/_app/$orgSlug/channels/$channelId/settings/integrations' - | '/_app/$orgSlug/channels/$channelId/settings/overview' - | '/_app/$orgSlug/chat/$id/files/media' - | '/_app/$orgSlug/channels/$channelId/settings/' - | '/_app/$orgSlug/chat/$id/files/' - fileRoutesById: FileRoutesById + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | "/" + | "/$orgSlug" + | "/ui" + | "/auth/callback" + | "/auth/desktop-callback" + | "/auth/desktop-login" + | "/auth/login" + | "/join/$slug" + | "/$orgSlug/my-settings" + | "/$orgSlug/notifications" + | "/$orgSlug/settings" + | "/onboarding/setup-organization" + | "/ui/agent-steps" + | "/dev/embeds/demo" + | "/dev/embeds/github" + | "/dev/embeds/openstatus" + | "/dev/embeds/railway" + | "/$orgSlug/" + | "/onboarding/" + | "/select-organization/" + | "/dev/embeds/" + | "/$orgSlug/settings/chat-sync" + | "/$orgSlug/settings/integrations" + | "/$orgSlug/chat/$id" + | "/$orgSlug/my-settings/desktop" + | "/$orgSlug/my-settings/linked-accounts" + | "/$orgSlug/my-settings/notifications" + | "/$orgSlug/my-settings/profile" + | "/$orgSlug/notifications/dms" + | "/$orgSlug/notifications/general" + | "/$orgSlug/notifications/threads" + | "/$orgSlug/profile/$userId" + | "/$orgSlug/settings/authentication" + | "/$orgSlug/settings/connect-invites" + | "/$orgSlug/settings/custom-emojis" + | "/$orgSlug/settings/debug" + | "/$orgSlug/settings/invitations" + | "/$orgSlug/settings/team" + | "/$orgSlug/chat/" + | "/$orgSlug/my-settings/" + | "/$orgSlug/notifications/" + | "/$orgSlug/settings/" + | "/$orgSlug/channels/$channelId/settings" + | "/$orgSlug/settings/chat-sync/$connectionId" + | "/$orgSlug/settings/integrations/$integrationId" + | "/$orgSlug/settings/integrations/installed" + | "/$orgSlug/settings/integrations/marketplace" + | "/$orgSlug/settings/integrations/your-apps" + | "/$orgSlug/chat/$id/" + | "/$orgSlug/settings/chat-sync/" + | "/$orgSlug/settings/integrations/" + | "/$orgSlug/channels/$channelId/settings/connect" + | "/$orgSlug/channels/$channelId/settings/integrations" + | "/$orgSlug/channels/$channelId/settings/overview" + | "/$orgSlug/chat/$id/files/media" + | "/$orgSlug/channels/$channelId/settings/" + | "/$orgSlug/chat/$id/files/" + fileRoutesByTo: FileRoutesByTo + to: + | "/" + | "/ui" + | "/auth/callback" + | "/auth/desktop-callback" + | "/auth/desktop-login" + | "/auth/login" + | "/join/$slug" + | "/onboarding/setup-organization" + | "/ui/agent-steps" + | "/dev/embeds/demo" + | "/dev/embeds/github" + | "/dev/embeds/openstatus" + | "/dev/embeds/railway" + | "/$orgSlug" + | "/onboarding" + | "/select-organization" + | "/dev/embeds" + | "/$orgSlug/my-settings/desktop" + | "/$orgSlug/my-settings/linked-accounts" + | "/$orgSlug/my-settings/notifications" + | "/$orgSlug/my-settings/profile" + | "/$orgSlug/notifications/dms" + | "/$orgSlug/notifications/general" + | "/$orgSlug/notifications/threads" + | "/$orgSlug/profile/$userId" + | "/$orgSlug/settings/authentication" + | "/$orgSlug/settings/connect-invites" + | "/$orgSlug/settings/custom-emojis" + | "/$orgSlug/settings/debug" + | "/$orgSlug/settings/invitations" + | "/$orgSlug/settings/team" + | "/$orgSlug/chat" + | "/$orgSlug/my-settings" + | "/$orgSlug/notifications" + | "/$orgSlug/settings" + | "/$orgSlug/settings/chat-sync/$connectionId" + | "/$orgSlug/settings/integrations/$integrationId" + | "/$orgSlug/settings/integrations/installed" + | "/$orgSlug/settings/integrations/marketplace" + | "/$orgSlug/settings/integrations/your-apps" + | "/$orgSlug/chat/$id" + | "/$orgSlug/settings/chat-sync" + | "/$orgSlug/settings/integrations" + | "/$orgSlug/channels/$channelId/settings/connect" + | "/$orgSlug/channels/$channelId/settings/integrations" + | "/$orgSlug/channels/$channelId/settings/overview" + | "/$orgSlug/chat/$id/files/media" + | "/$orgSlug/channels/$channelId/settings" + | "/$orgSlug/chat/$id/files" + id: + | "__root__" + | "/_app" + | "/_dev" + | "/_app/$orgSlug" + | "/_dev/ui" + | "/auth/callback" + | "/auth/desktop-callback" + | "/auth/desktop-login" + | "/auth/login" + | "/join/$slug" + | "/_app/" + | "/_app/$orgSlug/my-settings" + | "/_app/$orgSlug/notifications" + | "/_app/$orgSlug/settings" + | "/_app/onboarding/setup-organization" + | "/_dev/ui/agent-steps" + | "/dev/embeds/demo" + | "/dev/embeds/github" + | "/dev/embeds/openstatus" + | "/dev/embeds/railway" + | "/_app/$orgSlug/" + | "/_app/onboarding/" + | "/_app/select-organization/" + | "/dev/embeds/" + | "/_app/$orgSlug/settings/chat-sync" + | "/_app/$orgSlug/settings/integrations" + | "/_app/$orgSlug/chat/$id" + | "/_app/$orgSlug/my-settings/desktop" + | "/_app/$orgSlug/my-settings/linked-accounts" + | "/_app/$orgSlug/my-settings/notifications" + | "/_app/$orgSlug/my-settings/profile" + | "/_app/$orgSlug/notifications/dms" + | "/_app/$orgSlug/notifications/general" + | "/_app/$orgSlug/notifications/threads" + | "/_app/$orgSlug/profile/$userId" + | "/_app/$orgSlug/settings/authentication" + | "/_app/$orgSlug/settings/connect-invites" + | "/_app/$orgSlug/settings/custom-emojis" + | "/_app/$orgSlug/settings/debug" + | "/_app/$orgSlug/settings/invitations" + | "/_app/$orgSlug/settings/team" + | "/_app/$orgSlug/chat/" + | "/_app/$orgSlug/my-settings/" + | "/_app/$orgSlug/notifications/" + | "/_app/$orgSlug/settings/" + | "/_app/$orgSlug/channels/$channelId/settings" + | "/_app/$orgSlug/settings/chat-sync/$connectionId" + | "/_app/$orgSlug/settings/integrations/$integrationId" + | "/_app/$orgSlug/settings/integrations/installed" + | "/_app/$orgSlug/settings/integrations/marketplace" + | "/_app/$orgSlug/settings/integrations/your-apps" + | "/_app/$orgSlug/chat/$id/" + | "/_app/$orgSlug/settings/chat-sync/" + | "/_app/$orgSlug/settings/integrations/" + | "/_app/$orgSlug/channels/$channelId/settings/connect" + | "/_app/$orgSlug/channels/$channelId/settings/integrations" + | "/_app/$orgSlug/channels/$channelId/settings/overview" + | "/_app/$orgSlug/chat/$id/files/media" + | "/_app/$orgSlug/channels/$channelId/settings/" + | "/_app/$orgSlug/chat/$id/files/" + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - AppLayoutRoute: typeof AppLayoutRouteWithChildren - DevLayoutRoute: typeof DevLayoutRouteWithChildren - AuthCallbackRoute: typeof AuthCallbackRoute - AuthDesktopCallbackRoute: typeof AuthDesktopCallbackRoute - AuthDesktopLoginRoute: typeof AuthDesktopLoginRoute - AuthLoginRoute: typeof AuthLoginRoute - JoinSlugRoute: typeof JoinSlugRoute - DevEmbedsDemoRoute: typeof DevEmbedsDemoRoute - DevEmbedsGithubRoute: typeof DevEmbedsGithubRoute - DevEmbedsOpenstatusRoute: typeof DevEmbedsOpenstatusRoute - DevEmbedsRailwayRoute: typeof DevEmbedsRailwayRoute - DevEmbedsIndexRoute: typeof DevEmbedsIndexRoute + AppLayoutRoute: typeof AppLayoutRouteWithChildren + DevLayoutRoute: typeof DevLayoutRouteWithChildren + AuthCallbackRoute: typeof AuthCallbackRoute + AuthDesktopCallbackRoute: typeof AuthDesktopCallbackRoute + AuthDesktopLoginRoute: typeof AuthDesktopLoginRoute + AuthLoginRoute: typeof AuthLoginRoute + JoinSlugRoute: typeof JoinSlugRoute + DevEmbedsDemoRoute: typeof DevEmbedsDemoRoute + DevEmbedsGithubRoute: typeof DevEmbedsGithubRoute + DevEmbedsOpenstatusRoute: typeof DevEmbedsOpenstatusRoute + DevEmbedsRailwayRoute: typeof DevEmbedsRailwayRoute + DevEmbedsIndexRoute: typeof DevEmbedsIndexRoute } -declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/_dev': { - id: '/_dev' - path: '' - fullPath: '/' - preLoaderRoute: typeof DevLayoutRouteImport - parentRoute: typeof rootRouteImport - } - '/_app': { - id: '/_app' - path: '' - fullPath: '/' - preLoaderRoute: typeof AppLayoutRouteImport - parentRoute: typeof rootRouteImport - } - '/_app/': { - id: '/_app/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof AppIndexRouteImport - parentRoute: typeof AppLayoutRoute - } - '/join/$slug': { - id: '/join/$slug' - path: '/join/$slug' - fullPath: '/join/$slug' - preLoaderRoute: typeof JoinSlugRouteImport - parentRoute: typeof rootRouteImport - } - '/auth/login': { - id: '/auth/login' - path: '/auth/login' - fullPath: '/auth/login' - preLoaderRoute: typeof AuthLoginRouteImport - parentRoute: typeof rootRouteImport - } - '/auth/desktop-login': { - id: '/auth/desktop-login' - path: '/auth/desktop-login' - fullPath: '/auth/desktop-login' - preLoaderRoute: typeof AuthDesktopLoginRouteImport - parentRoute: typeof rootRouteImport - } - '/auth/desktop-callback': { - id: '/auth/desktop-callback' - path: '/auth/desktop-callback' - fullPath: '/auth/desktop-callback' - preLoaderRoute: typeof AuthDesktopCallbackRouteImport - parentRoute: typeof rootRouteImport - } - '/auth/callback': { - id: '/auth/callback' - path: '/auth/callback' - fullPath: '/auth/callback' - preLoaderRoute: typeof AuthCallbackRouteImport - parentRoute: typeof rootRouteImport - } - '/_dev/ui': { - id: '/_dev/ui' - path: '/ui' - fullPath: '/ui' - preLoaderRoute: typeof DevUiLayoutRouteImport - parentRoute: typeof DevLayoutRoute - } - '/_app/$orgSlug': { - id: '/_app/$orgSlug' - path: '/$orgSlug' - fullPath: '/$orgSlug' - preLoaderRoute: typeof AppOrgSlugLayoutRouteImport - parentRoute: typeof AppLayoutRoute - } - '/dev/embeds/': { - id: '/dev/embeds/' - path: '/dev/embeds' - fullPath: '/dev/embeds/' - preLoaderRoute: typeof DevEmbedsIndexRouteImport - parentRoute: typeof rootRouteImport - } - '/_app/select-organization/': { - id: '/_app/select-organization/' - path: '/select-organization' - fullPath: '/select-organization/' - preLoaderRoute: typeof AppSelectOrganizationIndexRouteImport - parentRoute: typeof AppLayoutRoute - } - '/_app/onboarding/': { - id: '/_app/onboarding/' - path: '/onboarding' - fullPath: '/onboarding/' - preLoaderRoute: typeof AppOnboardingIndexRouteImport - parentRoute: typeof AppLayoutRoute - } - '/_app/$orgSlug/': { - id: '/_app/$orgSlug/' - path: '/' - fullPath: '/$orgSlug/' - preLoaderRoute: typeof AppOrgSlugIndexRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/dev/embeds/railway': { - id: '/dev/embeds/railway' - path: '/dev/embeds/railway' - fullPath: '/dev/embeds/railway' - preLoaderRoute: typeof DevEmbedsRailwayRouteImport - parentRoute: typeof rootRouteImport - } - '/dev/embeds/openstatus': { - id: '/dev/embeds/openstatus' - path: '/dev/embeds/openstatus' - fullPath: '/dev/embeds/openstatus' - preLoaderRoute: typeof DevEmbedsOpenstatusRouteImport - parentRoute: typeof rootRouteImport - } - '/dev/embeds/github': { - id: '/dev/embeds/github' - path: '/dev/embeds/github' - fullPath: '/dev/embeds/github' - preLoaderRoute: typeof DevEmbedsGithubRouteImport - parentRoute: typeof rootRouteImport - } - '/dev/embeds/demo': { - id: '/dev/embeds/demo' - path: '/dev/embeds/demo' - fullPath: '/dev/embeds/demo' - preLoaderRoute: typeof DevEmbedsDemoRouteImport - parentRoute: typeof rootRouteImport - } - '/_dev/ui/agent-steps': { - id: '/_dev/ui/agent-steps' - path: '/agent-steps' - fullPath: '/ui/agent-steps' - preLoaderRoute: typeof DevUiAgentStepsRouteImport - parentRoute: typeof DevUiLayoutRoute - } - '/_app/onboarding/setup-organization': { - id: '/_app/onboarding/setup-organization' - path: '/onboarding/setup-organization' - fullPath: '/onboarding/setup-organization' - preLoaderRoute: typeof AppOnboardingSetupOrganizationRouteImport - parentRoute: typeof AppLayoutRoute - } - '/_app/$orgSlug/settings': { - id: '/_app/$orgSlug/settings' - path: '/settings' - fullPath: '/$orgSlug/settings' - preLoaderRoute: typeof AppOrgSlugSettingsLayoutRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/notifications': { - id: '/_app/$orgSlug/notifications' - path: '/notifications' - fullPath: '/$orgSlug/notifications' - preLoaderRoute: typeof AppOrgSlugNotificationsLayoutRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/my-settings': { - id: '/_app/$orgSlug/my-settings' - path: '/my-settings' - fullPath: '/$orgSlug/my-settings' - preLoaderRoute: typeof AppOrgSlugMySettingsLayoutRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/settings/': { - id: '/_app/$orgSlug/settings/' - path: '/' - fullPath: '/$orgSlug/settings/' - preLoaderRoute: typeof AppOrgSlugSettingsIndexRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/notifications/': { - id: '/_app/$orgSlug/notifications/' - path: '/' - fullPath: '/$orgSlug/notifications/' - preLoaderRoute: typeof AppOrgSlugNotificationsIndexRouteImport - parentRoute: typeof AppOrgSlugNotificationsLayoutRoute - } - '/_app/$orgSlug/my-settings/': { - id: '/_app/$orgSlug/my-settings/' - path: '/' - fullPath: '/$orgSlug/my-settings/' - preLoaderRoute: typeof AppOrgSlugMySettingsIndexRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - '/_app/$orgSlug/chat/': { - id: '/_app/$orgSlug/chat/' - path: '/chat' - fullPath: '/$orgSlug/chat/' - preLoaderRoute: typeof AppOrgSlugChatIndexRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/settings/team': { - id: '/_app/$orgSlug/settings/team' - path: '/team' - fullPath: '/$orgSlug/settings/team' - preLoaderRoute: typeof AppOrgSlugSettingsTeamRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/invitations': { - id: '/_app/$orgSlug/settings/invitations' - path: '/invitations' - fullPath: '/$orgSlug/settings/invitations' - preLoaderRoute: typeof AppOrgSlugSettingsInvitationsRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/debug': { - id: '/_app/$orgSlug/settings/debug' - path: '/debug' - fullPath: '/$orgSlug/settings/debug' - preLoaderRoute: typeof AppOrgSlugSettingsDebugRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/custom-emojis': { - id: '/_app/$orgSlug/settings/custom-emojis' - path: '/custom-emojis' - fullPath: '/$orgSlug/settings/custom-emojis' - preLoaderRoute: typeof AppOrgSlugSettingsCustomEmojisRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/connect-invites': { - id: '/_app/$orgSlug/settings/connect-invites' - path: '/connect-invites' - fullPath: '/$orgSlug/settings/connect-invites' - preLoaderRoute: typeof AppOrgSlugSettingsConnectInvitesRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/authentication': { - id: '/_app/$orgSlug/settings/authentication' - path: '/authentication' - fullPath: '/$orgSlug/settings/authentication' - preLoaderRoute: typeof AppOrgSlugSettingsAuthenticationRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/profile/$userId': { - id: '/_app/$orgSlug/profile/$userId' - path: '/profile/$userId' - fullPath: '/$orgSlug/profile/$userId' - preLoaderRoute: typeof AppOrgSlugProfileUserIdRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/notifications/threads': { - id: '/_app/$orgSlug/notifications/threads' - path: '/threads' - fullPath: '/$orgSlug/notifications/threads' - preLoaderRoute: typeof AppOrgSlugNotificationsThreadsRouteImport - parentRoute: typeof AppOrgSlugNotificationsLayoutRoute - } - '/_app/$orgSlug/notifications/general': { - id: '/_app/$orgSlug/notifications/general' - path: '/general' - fullPath: '/$orgSlug/notifications/general' - preLoaderRoute: typeof AppOrgSlugNotificationsGeneralRouteImport - parentRoute: typeof AppOrgSlugNotificationsLayoutRoute - } - '/_app/$orgSlug/notifications/dms': { - id: '/_app/$orgSlug/notifications/dms' - path: '/dms' - fullPath: '/$orgSlug/notifications/dms' - preLoaderRoute: typeof AppOrgSlugNotificationsDmsRouteImport - parentRoute: typeof AppOrgSlugNotificationsLayoutRoute - } - '/_app/$orgSlug/my-settings/profile': { - id: '/_app/$orgSlug/my-settings/profile' - path: '/profile' - fullPath: '/$orgSlug/my-settings/profile' - preLoaderRoute: typeof AppOrgSlugMySettingsProfileRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - '/_app/$orgSlug/my-settings/notifications': { - id: '/_app/$orgSlug/my-settings/notifications' - path: '/notifications' - fullPath: '/$orgSlug/my-settings/notifications' - preLoaderRoute: typeof AppOrgSlugMySettingsNotificationsRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - '/_app/$orgSlug/my-settings/linked-accounts': { - id: '/_app/$orgSlug/my-settings/linked-accounts' - path: '/linked-accounts' - fullPath: '/$orgSlug/my-settings/linked-accounts' - preLoaderRoute: typeof AppOrgSlugMySettingsLinkedAccountsRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - '/_app/$orgSlug/my-settings/desktop': { - id: '/_app/$orgSlug/my-settings/desktop' - path: '/desktop' - fullPath: '/$orgSlug/my-settings/desktop' - preLoaderRoute: typeof AppOrgSlugMySettingsDesktopRouteImport - parentRoute: typeof AppOrgSlugMySettingsLayoutRoute - } - '/_app/$orgSlug/chat/$id': { - id: '/_app/$orgSlug/chat/$id' - path: '/chat/$id' - fullPath: '/$orgSlug/chat/$id' - preLoaderRoute: typeof AppOrgSlugChatIdRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/settings/integrations': { - id: '/_app/$orgSlug/settings/integrations' - path: '/integrations' - fullPath: '/$orgSlug/settings/integrations' - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/chat-sync': { - id: '/_app/$orgSlug/settings/chat-sync' - path: '/chat-sync' - fullPath: '/$orgSlug/settings/chat-sync' - preLoaderRoute: typeof AppOrgSlugSettingsChatSyncLayoutRouteImport - parentRoute: typeof AppOrgSlugSettingsLayoutRoute - } - '/_app/$orgSlug/settings/integrations/': { - id: '/_app/$orgSlug/settings/integrations/' - path: '/' - fullPath: '/$orgSlug/settings/integrations/' - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsIndexRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - '/_app/$orgSlug/settings/chat-sync/': { - id: '/_app/$orgSlug/settings/chat-sync/' - path: '/' - fullPath: '/$orgSlug/settings/chat-sync/' - preLoaderRoute: typeof AppOrgSlugSettingsChatSyncIndexRouteImport - parentRoute: typeof AppOrgSlugSettingsChatSyncLayoutRoute - } - '/_app/$orgSlug/chat/$id/': { - id: '/_app/$orgSlug/chat/$id/' - path: '/' - fullPath: '/$orgSlug/chat/$id/' - preLoaderRoute: typeof AppOrgSlugChatIdIndexRouteImport - parentRoute: typeof AppOrgSlugChatIdRoute - } - '/_app/$orgSlug/settings/integrations/your-apps': { - id: '/_app/$orgSlug/settings/integrations/your-apps' - path: '/your-apps' - fullPath: '/$orgSlug/settings/integrations/your-apps' - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsYourAppsRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - '/_app/$orgSlug/settings/integrations/marketplace': { - id: '/_app/$orgSlug/settings/integrations/marketplace' - path: '/marketplace' - fullPath: '/$orgSlug/settings/integrations/marketplace' - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsMarketplaceRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - '/_app/$orgSlug/settings/integrations/installed': { - id: '/_app/$orgSlug/settings/integrations/installed' - path: '/installed' - fullPath: '/$orgSlug/settings/integrations/installed' - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsInstalledRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - '/_app/$orgSlug/settings/integrations/$integrationId': { - id: '/_app/$orgSlug/settings/integrations/$integrationId' - path: '/$integrationId' - fullPath: '/$orgSlug/settings/integrations/$integrationId' - preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport - parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute - } - '/_app/$orgSlug/settings/chat-sync/$connectionId': { - id: '/_app/$orgSlug/settings/chat-sync/$connectionId' - path: '/$connectionId' - fullPath: '/$orgSlug/settings/chat-sync/$connectionId' - preLoaderRoute: typeof AppOrgSlugSettingsChatSyncConnectionIdRouteImport - parentRoute: typeof AppOrgSlugSettingsChatSyncLayoutRoute - } - '/_app/$orgSlug/channels/$channelId/settings': { - id: '/_app/$orgSlug/channels/$channelId/settings' - path: '/channels/$channelId/settings' - fullPath: '/$orgSlug/channels/$channelId/settings' - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport - parentRoute: typeof AppOrgSlugLayoutRoute - } - '/_app/$orgSlug/chat/$id/files/': { - id: '/_app/$orgSlug/chat/$id/files/' - path: '/files' - fullPath: '/$orgSlug/chat/$id/files/' - preLoaderRoute: typeof AppOrgSlugChatIdFilesIndexRouteImport - parentRoute: typeof AppOrgSlugChatIdRoute - } - '/_app/$orgSlug/channels/$channelId/settings/': { - id: '/_app/$orgSlug/channels/$channelId/settings/' - path: '/' - fullPath: '/$orgSlug/channels/$channelId/settings/' - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsIndexRouteImport - parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute - } - '/_app/$orgSlug/chat/$id/files/media': { - id: '/_app/$orgSlug/chat/$id/files/media' - path: '/files/media' - fullPath: '/$orgSlug/chat/$id/files/media' - preLoaderRoute: typeof AppOrgSlugChatIdFilesMediaRouteImport - parentRoute: typeof AppOrgSlugChatIdRoute - } - '/_app/$orgSlug/channels/$channelId/settings/overview': { - id: '/_app/$orgSlug/channels/$channelId/settings/overview' - path: '/overview' - fullPath: '/$orgSlug/channels/$channelId/settings/overview' - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport - parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute - } - '/_app/$orgSlug/channels/$channelId/settings/integrations': { - id: '/_app/$orgSlug/channels/$channelId/settings/integrations' - path: '/integrations' - fullPath: '/$orgSlug/channels/$channelId/settings/integrations' - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport - parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute - } - '/_app/$orgSlug/channels/$channelId/settings/connect': { - id: '/_app/$orgSlug/channels/$channelId/settings/connect' - path: '/connect' - fullPath: '/$orgSlug/channels/$channelId/settings/connect' - preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsConnectRouteImport - parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute - } - } +declare module "@tanstack/react-router" { + interface FileRoutesByPath { + "/_dev": { + id: "/_dev" + path: "" + fullPath: "/" + preLoaderRoute: typeof DevLayoutRouteImport + parentRoute: typeof rootRouteImport + } + "/_app": { + id: "/_app" + path: "" + fullPath: "/" + preLoaderRoute: typeof AppLayoutRouteImport + parentRoute: typeof rootRouteImport + } + "/_app/": { + id: "/_app/" + path: "/" + fullPath: "/" + preLoaderRoute: typeof AppIndexRouteImport + parentRoute: typeof AppLayoutRoute + } + "/join/$slug": { + id: "/join/$slug" + path: "/join/$slug" + fullPath: "/join/$slug" + preLoaderRoute: typeof JoinSlugRouteImport + parentRoute: typeof rootRouteImport + } + "/auth/login": { + id: "/auth/login" + path: "/auth/login" + fullPath: "/auth/login" + preLoaderRoute: typeof AuthLoginRouteImport + parentRoute: typeof rootRouteImport + } + "/auth/desktop-login": { + id: "/auth/desktop-login" + path: "/auth/desktop-login" + fullPath: "/auth/desktop-login" + preLoaderRoute: typeof AuthDesktopLoginRouteImport + parentRoute: typeof rootRouteImport + } + "/auth/desktop-callback": { + id: "/auth/desktop-callback" + path: "/auth/desktop-callback" + fullPath: "/auth/desktop-callback" + preLoaderRoute: typeof AuthDesktopCallbackRouteImport + parentRoute: typeof rootRouteImport + } + "/auth/callback": { + id: "/auth/callback" + path: "/auth/callback" + fullPath: "/auth/callback" + preLoaderRoute: typeof AuthCallbackRouteImport + parentRoute: typeof rootRouteImport + } + "/_dev/ui": { + id: "/_dev/ui" + path: "/ui" + fullPath: "/ui" + preLoaderRoute: typeof DevUiLayoutRouteImport + parentRoute: typeof DevLayoutRoute + } + "/_app/$orgSlug": { + id: "/_app/$orgSlug" + path: "/$orgSlug" + fullPath: "/$orgSlug" + preLoaderRoute: typeof AppOrgSlugLayoutRouteImport + parentRoute: typeof AppLayoutRoute + } + "/dev/embeds/": { + id: "/dev/embeds/" + path: "/dev/embeds" + fullPath: "/dev/embeds/" + preLoaderRoute: typeof DevEmbedsIndexRouteImport + parentRoute: typeof rootRouteImport + } + "/_app/select-organization/": { + id: "/_app/select-organization/" + path: "/select-organization" + fullPath: "/select-organization/" + preLoaderRoute: typeof AppSelectOrganizationIndexRouteImport + parentRoute: typeof AppLayoutRoute + } + "/_app/onboarding/": { + id: "/_app/onboarding/" + path: "/onboarding" + fullPath: "/onboarding/" + preLoaderRoute: typeof AppOnboardingIndexRouteImport + parentRoute: typeof AppLayoutRoute + } + "/_app/$orgSlug/": { + id: "/_app/$orgSlug/" + path: "/" + fullPath: "/$orgSlug/" + preLoaderRoute: typeof AppOrgSlugIndexRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/dev/embeds/railway": { + id: "/dev/embeds/railway" + path: "/dev/embeds/railway" + fullPath: "/dev/embeds/railway" + preLoaderRoute: typeof DevEmbedsRailwayRouteImport + parentRoute: typeof rootRouteImport + } + "/dev/embeds/openstatus": { + id: "/dev/embeds/openstatus" + path: "/dev/embeds/openstatus" + fullPath: "/dev/embeds/openstatus" + preLoaderRoute: typeof DevEmbedsOpenstatusRouteImport + parentRoute: typeof rootRouteImport + } + "/dev/embeds/github": { + id: "/dev/embeds/github" + path: "/dev/embeds/github" + fullPath: "/dev/embeds/github" + preLoaderRoute: typeof DevEmbedsGithubRouteImport + parentRoute: typeof rootRouteImport + } + "/dev/embeds/demo": { + id: "/dev/embeds/demo" + path: "/dev/embeds/demo" + fullPath: "/dev/embeds/demo" + preLoaderRoute: typeof DevEmbedsDemoRouteImport + parentRoute: typeof rootRouteImport + } + "/_dev/ui/agent-steps": { + id: "/_dev/ui/agent-steps" + path: "/agent-steps" + fullPath: "/ui/agent-steps" + preLoaderRoute: typeof DevUiAgentStepsRouteImport + parentRoute: typeof DevUiLayoutRoute + } + "/_app/onboarding/setup-organization": { + id: "/_app/onboarding/setup-organization" + path: "/onboarding/setup-organization" + fullPath: "/onboarding/setup-organization" + preLoaderRoute: typeof AppOnboardingSetupOrganizationRouteImport + parentRoute: typeof AppLayoutRoute + } + "/_app/$orgSlug/settings": { + id: "/_app/$orgSlug/settings" + path: "/settings" + fullPath: "/$orgSlug/settings" + preLoaderRoute: typeof AppOrgSlugSettingsLayoutRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/notifications": { + id: "/_app/$orgSlug/notifications" + path: "/notifications" + fullPath: "/$orgSlug/notifications" + preLoaderRoute: typeof AppOrgSlugNotificationsLayoutRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/my-settings": { + id: "/_app/$orgSlug/my-settings" + path: "/my-settings" + fullPath: "/$orgSlug/my-settings" + preLoaderRoute: typeof AppOrgSlugMySettingsLayoutRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/settings/": { + id: "/_app/$orgSlug/settings/" + path: "/" + fullPath: "/$orgSlug/settings/" + preLoaderRoute: typeof AppOrgSlugSettingsIndexRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/notifications/": { + id: "/_app/$orgSlug/notifications/" + path: "/" + fullPath: "/$orgSlug/notifications/" + preLoaderRoute: typeof AppOrgSlugNotificationsIndexRouteImport + parentRoute: typeof AppOrgSlugNotificationsLayoutRoute + } + "/_app/$orgSlug/my-settings/": { + id: "/_app/$orgSlug/my-settings/" + path: "/" + fullPath: "/$orgSlug/my-settings/" + preLoaderRoute: typeof AppOrgSlugMySettingsIndexRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + "/_app/$orgSlug/chat/": { + id: "/_app/$orgSlug/chat/" + path: "/chat" + fullPath: "/$orgSlug/chat/" + preLoaderRoute: typeof AppOrgSlugChatIndexRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/settings/team": { + id: "/_app/$orgSlug/settings/team" + path: "/team" + fullPath: "/$orgSlug/settings/team" + preLoaderRoute: typeof AppOrgSlugSettingsTeamRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/invitations": { + id: "/_app/$orgSlug/settings/invitations" + path: "/invitations" + fullPath: "/$orgSlug/settings/invitations" + preLoaderRoute: typeof AppOrgSlugSettingsInvitationsRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/debug": { + id: "/_app/$orgSlug/settings/debug" + path: "/debug" + fullPath: "/$orgSlug/settings/debug" + preLoaderRoute: typeof AppOrgSlugSettingsDebugRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/custom-emojis": { + id: "/_app/$orgSlug/settings/custom-emojis" + path: "/custom-emojis" + fullPath: "/$orgSlug/settings/custom-emojis" + preLoaderRoute: typeof AppOrgSlugSettingsCustomEmojisRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/connect-invites": { + id: "/_app/$orgSlug/settings/connect-invites" + path: "/connect-invites" + fullPath: "/$orgSlug/settings/connect-invites" + preLoaderRoute: typeof AppOrgSlugSettingsConnectInvitesRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/authentication": { + id: "/_app/$orgSlug/settings/authentication" + path: "/authentication" + fullPath: "/$orgSlug/settings/authentication" + preLoaderRoute: typeof AppOrgSlugSettingsAuthenticationRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/profile/$userId": { + id: "/_app/$orgSlug/profile/$userId" + path: "/profile/$userId" + fullPath: "/$orgSlug/profile/$userId" + preLoaderRoute: typeof AppOrgSlugProfileUserIdRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/notifications/threads": { + id: "/_app/$orgSlug/notifications/threads" + path: "/threads" + fullPath: "/$orgSlug/notifications/threads" + preLoaderRoute: typeof AppOrgSlugNotificationsThreadsRouteImport + parentRoute: typeof AppOrgSlugNotificationsLayoutRoute + } + "/_app/$orgSlug/notifications/general": { + id: "/_app/$orgSlug/notifications/general" + path: "/general" + fullPath: "/$orgSlug/notifications/general" + preLoaderRoute: typeof AppOrgSlugNotificationsGeneralRouteImport + parentRoute: typeof AppOrgSlugNotificationsLayoutRoute + } + "/_app/$orgSlug/notifications/dms": { + id: "/_app/$orgSlug/notifications/dms" + path: "/dms" + fullPath: "/$orgSlug/notifications/dms" + preLoaderRoute: typeof AppOrgSlugNotificationsDmsRouteImport + parentRoute: typeof AppOrgSlugNotificationsLayoutRoute + } + "/_app/$orgSlug/my-settings/profile": { + id: "/_app/$orgSlug/my-settings/profile" + path: "/profile" + fullPath: "/$orgSlug/my-settings/profile" + preLoaderRoute: typeof AppOrgSlugMySettingsProfileRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + "/_app/$orgSlug/my-settings/notifications": { + id: "/_app/$orgSlug/my-settings/notifications" + path: "/notifications" + fullPath: "/$orgSlug/my-settings/notifications" + preLoaderRoute: typeof AppOrgSlugMySettingsNotificationsRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + "/_app/$orgSlug/my-settings/linked-accounts": { + id: "/_app/$orgSlug/my-settings/linked-accounts" + path: "/linked-accounts" + fullPath: "/$orgSlug/my-settings/linked-accounts" + preLoaderRoute: typeof AppOrgSlugMySettingsLinkedAccountsRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + "/_app/$orgSlug/my-settings/desktop": { + id: "/_app/$orgSlug/my-settings/desktop" + path: "/desktop" + fullPath: "/$orgSlug/my-settings/desktop" + preLoaderRoute: typeof AppOrgSlugMySettingsDesktopRouteImport + parentRoute: typeof AppOrgSlugMySettingsLayoutRoute + } + "/_app/$orgSlug/chat/$id": { + id: "/_app/$orgSlug/chat/$id" + path: "/chat/$id" + fullPath: "/$orgSlug/chat/$id" + preLoaderRoute: typeof AppOrgSlugChatIdRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/settings/integrations": { + id: "/_app/$orgSlug/settings/integrations" + path: "/integrations" + fullPath: "/$orgSlug/settings/integrations" + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/chat-sync": { + id: "/_app/$orgSlug/settings/chat-sync" + path: "/chat-sync" + fullPath: "/$orgSlug/settings/chat-sync" + preLoaderRoute: typeof AppOrgSlugSettingsChatSyncLayoutRouteImport + parentRoute: typeof AppOrgSlugSettingsLayoutRoute + } + "/_app/$orgSlug/settings/integrations/": { + id: "/_app/$orgSlug/settings/integrations/" + path: "/" + fullPath: "/$orgSlug/settings/integrations/" + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsIndexRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + "/_app/$orgSlug/settings/chat-sync/": { + id: "/_app/$orgSlug/settings/chat-sync/" + path: "/" + fullPath: "/$orgSlug/settings/chat-sync/" + preLoaderRoute: typeof AppOrgSlugSettingsChatSyncIndexRouteImport + parentRoute: typeof AppOrgSlugSettingsChatSyncLayoutRoute + } + "/_app/$orgSlug/chat/$id/": { + id: "/_app/$orgSlug/chat/$id/" + path: "/" + fullPath: "/$orgSlug/chat/$id/" + preLoaderRoute: typeof AppOrgSlugChatIdIndexRouteImport + parentRoute: typeof AppOrgSlugChatIdRoute + } + "/_app/$orgSlug/settings/integrations/your-apps": { + id: "/_app/$orgSlug/settings/integrations/your-apps" + path: "/your-apps" + fullPath: "/$orgSlug/settings/integrations/your-apps" + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsYourAppsRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + "/_app/$orgSlug/settings/integrations/marketplace": { + id: "/_app/$orgSlug/settings/integrations/marketplace" + path: "/marketplace" + fullPath: "/$orgSlug/settings/integrations/marketplace" + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsMarketplaceRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + "/_app/$orgSlug/settings/integrations/installed": { + id: "/_app/$orgSlug/settings/integrations/installed" + path: "/installed" + fullPath: "/$orgSlug/settings/integrations/installed" + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsInstalledRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + "/_app/$orgSlug/settings/integrations/$integrationId": { + id: "/_app/$orgSlug/settings/integrations/$integrationId" + path: "/$integrationId" + fullPath: "/$orgSlug/settings/integrations/$integrationId" + preLoaderRoute: typeof AppOrgSlugSettingsIntegrationsIntegrationIdRouteImport + parentRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRoute + } + "/_app/$orgSlug/settings/chat-sync/$connectionId": { + id: "/_app/$orgSlug/settings/chat-sync/$connectionId" + path: "/$connectionId" + fullPath: "/$orgSlug/settings/chat-sync/$connectionId" + preLoaderRoute: typeof AppOrgSlugSettingsChatSyncConnectionIdRouteImport + parentRoute: typeof AppOrgSlugSettingsChatSyncLayoutRoute + } + "/_app/$orgSlug/channels/$channelId/settings": { + id: "/_app/$orgSlug/channels/$channelId/settings" + path: "/channels/$channelId/settings" + fullPath: "/$orgSlug/channels/$channelId/settings" + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteImport + parentRoute: typeof AppOrgSlugLayoutRoute + } + "/_app/$orgSlug/chat/$id/files/": { + id: "/_app/$orgSlug/chat/$id/files/" + path: "/files" + fullPath: "/$orgSlug/chat/$id/files/" + preLoaderRoute: typeof AppOrgSlugChatIdFilesIndexRouteImport + parentRoute: typeof AppOrgSlugChatIdRoute + } + "/_app/$orgSlug/channels/$channelId/settings/": { + id: "/_app/$orgSlug/channels/$channelId/settings/" + path: "/" + fullPath: "/$orgSlug/channels/$channelId/settings/" + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsIndexRouteImport + parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute + } + "/_app/$orgSlug/chat/$id/files/media": { + id: "/_app/$orgSlug/chat/$id/files/media" + path: "/files/media" + fullPath: "/$orgSlug/chat/$id/files/media" + preLoaderRoute: typeof AppOrgSlugChatIdFilesMediaRouteImport + parentRoute: typeof AppOrgSlugChatIdRoute + } + "/_app/$orgSlug/channels/$channelId/settings/overview": { + id: "/_app/$orgSlug/channels/$channelId/settings/overview" + path: "/overview" + fullPath: "/$orgSlug/channels/$channelId/settings/overview" + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsOverviewRouteImport + parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute + } + "/_app/$orgSlug/channels/$channelId/settings/integrations": { + id: "/_app/$orgSlug/channels/$channelId/settings/integrations" + path: "/integrations" + fullPath: "/$orgSlug/channels/$channelId/settings/integrations" + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRouteImport + parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute + } + "/_app/$orgSlug/channels/$channelId/settings/connect": { + id: "/_app/$orgSlug/channels/$channelId/settings/connect" + path: "/connect" + fullPath: "/$orgSlug/channels/$channelId/settings/connect" + preLoaderRoute: typeof AppOrgSlugChannelsChannelIdSettingsConnectRouteImport + parentRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRoute + } + } } interface AppOrgSlugMySettingsLayoutRouteChildren { - AppOrgSlugMySettingsDesktopRoute: typeof AppOrgSlugMySettingsDesktopRoute - AppOrgSlugMySettingsLinkedAccountsRoute: typeof AppOrgSlugMySettingsLinkedAccountsRoute - AppOrgSlugMySettingsNotificationsRoute: typeof AppOrgSlugMySettingsNotificationsRoute - AppOrgSlugMySettingsProfileRoute: typeof AppOrgSlugMySettingsProfileRoute - AppOrgSlugMySettingsIndexRoute: typeof AppOrgSlugMySettingsIndexRoute + AppOrgSlugMySettingsDesktopRoute: typeof AppOrgSlugMySettingsDesktopRoute + AppOrgSlugMySettingsLinkedAccountsRoute: typeof AppOrgSlugMySettingsLinkedAccountsRoute + AppOrgSlugMySettingsNotificationsRoute: typeof AppOrgSlugMySettingsNotificationsRoute + AppOrgSlugMySettingsProfileRoute: typeof AppOrgSlugMySettingsProfileRoute + AppOrgSlugMySettingsIndexRoute: typeof AppOrgSlugMySettingsIndexRoute } -const AppOrgSlugMySettingsLayoutRouteChildren: AppOrgSlugMySettingsLayoutRouteChildren = - { - AppOrgSlugMySettingsDesktopRoute: AppOrgSlugMySettingsDesktopRoute, - AppOrgSlugMySettingsLinkedAccountsRoute: - AppOrgSlugMySettingsLinkedAccountsRoute, - AppOrgSlugMySettingsNotificationsRoute: - AppOrgSlugMySettingsNotificationsRoute, - AppOrgSlugMySettingsProfileRoute: AppOrgSlugMySettingsProfileRoute, - AppOrgSlugMySettingsIndexRoute: AppOrgSlugMySettingsIndexRoute, - } +const AppOrgSlugMySettingsLayoutRouteChildren: AppOrgSlugMySettingsLayoutRouteChildren = { + AppOrgSlugMySettingsDesktopRoute: AppOrgSlugMySettingsDesktopRoute, + AppOrgSlugMySettingsLinkedAccountsRoute: AppOrgSlugMySettingsLinkedAccountsRoute, + AppOrgSlugMySettingsNotificationsRoute: AppOrgSlugMySettingsNotificationsRoute, + AppOrgSlugMySettingsProfileRoute: AppOrgSlugMySettingsProfileRoute, + AppOrgSlugMySettingsIndexRoute: AppOrgSlugMySettingsIndexRoute, +} -const AppOrgSlugMySettingsLayoutRouteWithChildren = - AppOrgSlugMySettingsLayoutRoute._addFileChildren( - AppOrgSlugMySettingsLayoutRouteChildren, - ) +const AppOrgSlugMySettingsLayoutRouteWithChildren = AppOrgSlugMySettingsLayoutRoute._addFileChildren( + AppOrgSlugMySettingsLayoutRouteChildren, +) interface AppOrgSlugNotificationsLayoutRouteChildren { - AppOrgSlugNotificationsDmsRoute: typeof AppOrgSlugNotificationsDmsRoute - AppOrgSlugNotificationsGeneralRoute: typeof AppOrgSlugNotificationsGeneralRoute - AppOrgSlugNotificationsThreadsRoute: typeof AppOrgSlugNotificationsThreadsRoute - AppOrgSlugNotificationsIndexRoute: typeof AppOrgSlugNotificationsIndexRoute + AppOrgSlugNotificationsDmsRoute: typeof AppOrgSlugNotificationsDmsRoute + AppOrgSlugNotificationsGeneralRoute: typeof AppOrgSlugNotificationsGeneralRoute + AppOrgSlugNotificationsThreadsRoute: typeof AppOrgSlugNotificationsThreadsRoute + AppOrgSlugNotificationsIndexRoute: typeof AppOrgSlugNotificationsIndexRoute } -const AppOrgSlugNotificationsLayoutRouteChildren: AppOrgSlugNotificationsLayoutRouteChildren = - { - AppOrgSlugNotificationsDmsRoute: AppOrgSlugNotificationsDmsRoute, - AppOrgSlugNotificationsGeneralRoute: AppOrgSlugNotificationsGeneralRoute, - AppOrgSlugNotificationsThreadsRoute: AppOrgSlugNotificationsThreadsRoute, - AppOrgSlugNotificationsIndexRoute: AppOrgSlugNotificationsIndexRoute, - } +const AppOrgSlugNotificationsLayoutRouteChildren: AppOrgSlugNotificationsLayoutRouteChildren = { + AppOrgSlugNotificationsDmsRoute: AppOrgSlugNotificationsDmsRoute, + AppOrgSlugNotificationsGeneralRoute: AppOrgSlugNotificationsGeneralRoute, + AppOrgSlugNotificationsThreadsRoute: AppOrgSlugNotificationsThreadsRoute, + AppOrgSlugNotificationsIndexRoute: AppOrgSlugNotificationsIndexRoute, +} -const AppOrgSlugNotificationsLayoutRouteWithChildren = - AppOrgSlugNotificationsLayoutRoute._addFileChildren( - AppOrgSlugNotificationsLayoutRouteChildren, - ) +const AppOrgSlugNotificationsLayoutRouteWithChildren = AppOrgSlugNotificationsLayoutRoute._addFileChildren( + AppOrgSlugNotificationsLayoutRouteChildren, +) interface AppOrgSlugSettingsChatSyncLayoutRouteChildren { - AppOrgSlugSettingsChatSyncConnectionIdRoute: typeof AppOrgSlugSettingsChatSyncConnectionIdRoute - AppOrgSlugSettingsChatSyncIndexRoute: typeof AppOrgSlugSettingsChatSyncIndexRoute + AppOrgSlugSettingsChatSyncConnectionIdRoute: typeof AppOrgSlugSettingsChatSyncConnectionIdRoute + AppOrgSlugSettingsChatSyncIndexRoute: typeof AppOrgSlugSettingsChatSyncIndexRoute } -const AppOrgSlugSettingsChatSyncLayoutRouteChildren: AppOrgSlugSettingsChatSyncLayoutRouteChildren = - { - AppOrgSlugSettingsChatSyncConnectionIdRoute: - AppOrgSlugSettingsChatSyncConnectionIdRoute, - AppOrgSlugSettingsChatSyncIndexRoute: AppOrgSlugSettingsChatSyncIndexRoute, - } +const AppOrgSlugSettingsChatSyncLayoutRouteChildren: AppOrgSlugSettingsChatSyncLayoutRouteChildren = { + AppOrgSlugSettingsChatSyncConnectionIdRoute: AppOrgSlugSettingsChatSyncConnectionIdRoute, + AppOrgSlugSettingsChatSyncIndexRoute: AppOrgSlugSettingsChatSyncIndexRoute, +} const AppOrgSlugSettingsChatSyncLayoutRouteWithChildren = - AppOrgSlugSettingsChatSyncLayoutRoute._addFileChildren( - AppOrgSlugSettingsChatSyncLayoutRouteChildren, - ) + AppOrgSlugSettingsChatSyncLayoutRoute._addFileChildren(AppOrgSlugSettingsChatSyncLayoutRouteChildren) interface AppOrgSlugSettingsIntegrationsLayoutRouteChildren { - AppOrgSlugSettingsIntegrationsIntegrationIdRoute: typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute - AppOrgSlugSettingsIntegrationsInstalledRoute: typeof AppOrgSlugSettingsIntegrationsInstalledRoute - AppOrgSlugSettingsIntegrationsMarketplaceRoute: typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute - AppOrgSlugSettingsIntegrationsYourAppsRoute: typeof AppOrgSlugSettingsIntegrationsYourAppsRoute - AppOrgSlugSettingsIntegrationsIndexRoute: typeof AppOrgSlugSettingsIntegrationsIndexRoute + AppOrgSlugSettingsIntegrationsIntegrationIdRoute: typeof AppOrgSlugSettingsIntegrationsIntegrationIdRoute + AppOrgSlugSettingsIntegrationsInstalledRoute: typeof AppOrgSlugSettingsIntegrationsInstalledRoute + AppOrgSlugSettingsIntegrationsMarketplaceRoute: typeof AppOrgSlugSettingsIntegrationsMarketplaceRoute + AppOrgSlugSettingsIntegrationsYourAppsRoute: typeof AppOrgSlugSettingsIntegrationsYourAppsRoute + AppOrgSlugSettingsIntegrationsIndexRoute: typeof AppOrgSlugSettingsIntegrationsIndexRoute } -const AppOrgSlugSettingsIntegrationsLayoutRouteChildren: AppOrgSlugSettingsIntegrationsLayoutRouteChildren = - { - AppOrgSlugSettingsIntegrationsIntegrationIdRoute: - AppOrgSlugSettingsIntegrationsIntegrationIdRoute, - AppOrgSlugSettingsIntegrationsInstalledRoute: - AppOrgSlugSettingsIntegrationsInstalledRoute, - AppOrgSlugSettingsIntegrationsMarketplaceRoute: - AppOrgSlugSettingsIntegrationsMarketplaceRoute, - AppOrgSlugSettingsIntegrationsYourAppsRoute: - AppOrgSlugSettingsIntegrationsYourAppsRoute, - AppOrgSlugSettingsIntegrationsIndexRoute: - AppOrgSlugSettingsIntegrationsIndexRoute, - } +const AppOrgSlugSettingsIntegrationsLayoutRouteChildren: AppOrgSlugSettingsIntegrationsLayoutRouteChildren = { + AppOrgSlugSettingsIntegrationsIntegrationIdRoute: AppOrgSlugSettingsIntegrationsIntegrationIdRoute, + AppOrgSlugSettingsIntegrationsInstalledRoute: AppOrgSlugSettingsIntegrationsInstalledRoute, + AppOrgSlugSettingsIntegrationsMarketplaceRoute: AppOrgSlugSettingsIntegrationsMarketplaceRoute, + AppOrgSlugSettingsIntegrationsYourAppsRoute: AppOrgSlugSettingsIntegrationsYourAppsRoute, + AppOrgSlugSettingsIntegrationsIndexRoute: AppOrgSlugSettingsIntegrationsIndexRoute, +} const AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren = - AppOrgSlugSettingsIntegrationsLayoutRoute._addFileChildren( - AppOrgSlugSettingsIntegrationsLayoutRouteChildren, - ) + AppOrgSlugSettingsIntegrationsLayoutRoute._addFileChildren( + AppOrgSlugSettingsIntegrationsLayoutRouteChildren, + ) interface AppOrgSlugSettingsLayoutRouteChildren { - AppOrgSlugSettingsChatSyncLayoutRoute: typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren - AppOrgSlugSettingsIntegrationsLayoutRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren - AppOrgSlugSettingsAuthenticationRoute: typeof AppOrgSlugSettingsAuthenticationRoute - AppOrgSlugSettingsConnectInvitesRoute: typeof AppOrgSlugSettingsConnectInvitesRoute - AppOrgSlugSettingsCustomEmojisRoute: typeof AppOrgSlugSettingsCustomEmojisRoute - AppOrgSlugSettingsDebugRoute: typeof AppOrgSlugSettingsDebugRoute - AppOrgSlugSettingsInvitationsRoute: typeof AppOrgSlugSettingsInvitationsRoute - AppOrgSlugSettingsTeamRoute: typeof AppOrgSlugSettingsTeamRoute - AppOrgSlugSettingsIndexRoute: typeof AppOrgSlugSettingsIndexRoute + AppOrgSlugSettingsChatSyncLayoutRoute: typeof AppOrgSlugSettingsChatSyncLayoutRouteWithChildren + AppOrgSlugSettingsIntegrationsLayoutRoute: typeof AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren + AppOrgSlugSettingsAuthenticationRoute: typeof AppOrgSlugSettingsAuthenticationRoute + AppOrgSlugSettingsConnectInvitesRoute: typeof AppOrgSlugSettingsConnectInvitesRoute + AppOrgSlugSettingsCustomEmojisRoute: typeof AppOrgSlugSettingsCustomEmojisRoute + AppOrgSlugSettingsDebugRoute: typeof AppOrgSlugSettingsDebugRoute + AppOrgSlugSettingsInvitationsRoute: typeof AppOrgSlugSettingsInvitationsRoute + AppOrgSlugSettingsTeamRoute: typeof AppOrgSlugSettingsTeamRoute + AppOrgSlugSettingsIndexRoute: typeof AppOrgSlugSettingsIndexRoute } -const AppOrgSlugSettingsLayoutRouteChildren: AppOrgSlugSettingsLayoutRouteChildren = - { - AppOrgSlugSettingsChatSyncLayoutRoute: - AppOrgSlugSettingsChatSyncLayoutRouteWithChildren, - AppOrgSlugSettingsIntegrationsLayoutRoute: - AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren, - AppOrgSlugSettingsAuthenticationRoute: - AppOrgSlugSettingsAuthenticationRoute, - AppOrgSlugSettingsConnectInvitesRoute: - AppOrgSlugSettingsConnectInvitesRoute, - AppOrgSlugSettingsCustomEmojisRoute: AppOrgSlugSettingsCustomEmojisRoute, - AppOrgSlugSettingsDebugRoute: AppOrgSlugSettingsDebugRoute, - AppOrgSlugSettingsInvitationsRoute: AppOrgSlugSettingsInvitationsRoute, - AppOrgSlugSettingsTeamRoute: AppOrgSlugSettingsTeamRoute, - AppOrgSlugSettingsIndexRoute: AppOrgSlugSettingsIndexRoute, - } +const AppOrgSlugSettingsLayoutRouteChildren: AppOrgSlugSettingsLayoutRouteChildren = { + AppOrgSlugSettingsChatSyncLayoutRoute: AppOrgSlugSettingsChatSyncLayoutRouteWithChildren, + AppOrgSlugSettingsIntegrationsLayoutRoute: AppOrgSlugSettingsIntegrationsLayoutRouteWithChildren, + AppOrgSlugSettingsAuthenticationRoute: AppOrgSlugSettingsAuthenticationRoute, + AppOrgSlugSettingsConnectInvitesRoute: AppOrgSlugSettingsConnectInvitesRoute, + AppOrgSlugSettingsCustomEmojisRoute: AppOrgSlugSettingsCustomEmojisRoute, + AppOrgSlugSettingsDebugRoute: AppOrgSlugSettingsDebugRoute, + AppOrgSlugSettingsInvitationsRoute: AppOrgSlugSettingsInvitationsRoute, + AppOrgSlugSettingsTeamRoute: AppOrgSlugSettingsTeamRoute, + AppOrgSlugSettingsIndexRoute: AppOrgSlugSettingsIndexRoute, +} -const AppOrgSlugSettingsLayoutRouteWithChildren = - AppOrgSlugSettingsLayoutRoute._addFileChildren( - AppOrgSlugSettingsLayoutRouteChildren, - ) +const AppOrgSlugSettingsLayoutRouteWithChildren = AppOrgSlugSettingsLayoutRoute._addFileChildren( + AppOrgSlugSettingsLayoutRouteChildren, +) interface AppOrgSlugChatIdRouteChildren { - AppOrgSlugChatIdIndexRoute: typeof AppOrgSlugChatIdIndexRoute - AppOrgSlugChatIdFilesMediaRoute: typeof AppOrgSlugChatIdFilesMediaRoute - AppOrgSlugChatIdFilesIndexRoute: typeof AppOrgSlugChatIdFilesIndexRoute + AppOrgSlugChatIdIndexRoute: typeof AppOrgSlugChatIdIndexRoute + AppOrgSlugChatIdFilesMediaRoute: typeof AppOrgSlugChatIdFilesMediaRoute + AppOrgSlugChatIdFilesIndexRoute: typeof AppOrgSlugChatIdFilesIndexRoute } const AppOrgSlugChatIdRouteChildren: AppOrgSlugChatIdRouteChildren = { - AppOrgSlugChatIdIndexRoute: AppOrgSlugChatIdIndexRoute, - AppOrgSlugChatIdFilesMediaRoute: AppOrgSlugChatIdFilesMediaRoute, - AppOrgSlugChatIdFilesIndexRoute: AppOrgSlugChatIdFilesIndexRoute, + AppOrgSlugChatIdIndexRoute: AppOrgSlugChatIdIndexRoute, + AppOrgSlugChatIdFilesMediaRoute: AppOrgSlugChatIdFilesMediaRoute, + AppOrgSlugChatIdFilesIndexRoute: AppOrgSlugChatIdFilesIndexRoute, } -const AppOrgSlugChatIdRouteWithChildren = - AppOrgSlugChatIdRoute._addFileChildren(AppOrgSlugChatIdRouteChildren) +const AppOrgSlugChatIdRouteWithChildren = AppOrgSlugChatIdRoute._addFileChildren( + AppOrgSlugChatIdRouteChildren, +) interface AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren { - AppOrgSlugChannelsChannelIdSettingsConnectRoute: typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute - AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute: typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute - AppOrgSlugChannelsChannelIdSettingsOverviewRoute: typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute - AppOrgSlugChannelsChannelIdSettingsIndexRoute: typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute + AppOrgSlugChannelsChannelIdSettingsConnectRoute: typeof AppOrgSlugChannelsChannelIdSettingsConnectRoute + AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute: typeof AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute + AppOrgSlugChannelsChannelIdSettingsOverviewRoute: typeof AppOrgSlugChannelsChannelIdSettingsOverviewRoute + AppOrgSlugChannelsChannelIdSettingsIndexRoute: typeof AppOrgSlugChannelsChannelIdSettingsIndexRoute } const AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren: AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren = - { - AppOrgSlugChannelsChannelIdSettingsConnectRoute: - AppOrgSlugChannelsChannelIdSettingsConnectRoute, - AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute: - AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute, - AppOrgSlugChannelsChannelIdSettingsOverviewRoute: - AppOrgSlugChannelsChannelIdSettingsOverviewRoute, - AppOrgSlugChannelsChannelIdSettingsIndexRoute: - AppOrgSlugChannelsChannelIdSettingsIndexRoute, - } + { + AppOrgSlugChannelsChannelIdSettingsConnectRoute: AppOrgSlugChannelsChannelIdSettingsConnectRoute, + AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute: + AppOrgSlugChannelsChannelIdSettingsIntegrationsRoute, + AppOrgSlugChannelsChannelIdSettingsOverviewRoute: AppOrgSlugChannelsChannelIdSettingsOverviewRoute, + AppOrgSlugChannelsChannelIdSettingsIndexRoute: AppOrgSlugChannelsChannelIdSettingsIndexRoute, + } const AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren = - AppOrgSlugChannelsChannelIdSettingsLayoutRoute._addFileChildren( - AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren, - ) + AppOrgSlugChannelsChannelIdSettingsLayoutRoute._addFileChildren( + AppOrgSlugChannelsChannelIdSettingsLayoutRouteChildren, + ) interface AppOrgSlugLayoutRouteChildren { - AppOrgSlugMySettingsLayoutRoute: typeof AppOrgSlugMySettingsLayoutRouteWithChildren - AppOrgSlugNotificationsLayoutRoute: typeof AppOrgSlugNotificationsLayoutRouteWithChildren - AppOrgSlugSettingsLayoutRoute: typeof AppOrgSlugSettingsLayoutRouteWithChildren - AppOrgSlugIndexRoute: typeof AppOrgSlugIndexRoute - AppOrgSlugChatIdRoute: typeof AppOrgSlugChatIdRouteWithChildren - AppOrgSlugProfileUserIdRoute: typeof AppOrgSlugProfileUserIdRoute - AppOrgSlugChatIndexRoute: typeof AppOrgSlugChatIndexRoute - AppOrgSlugChannelsChannelIdSettingsLayoutRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren + AppOrgSlugMySettingsLayoutRoute: typeof AppOrgSlugMySettingsLayoutRouteWithChildren + AppOrgSlugNotificationsLayoutRoute: typeof AppOrgSlugNotificationsLayoutRouteWithChildren + AppOrgSlugSettingsLayoutRoute: typeof AppOrgSlugSettingsLayoutRouteWithChildren + AppOrgSlugIndexRoute: typeof AppOrgSlugIndexRoute + AppOrgSlugChatIdRoute: typeof AppOrgSlugChatIdRouteWithChildren + AppOrgSlugProfileUserIdRoute: typeof AppOrgSlugProfileUserIdRoute + AppOrgSlugChatIndexRoute: typeof AppOrgSlugChatIndexRoute + AppOrgSlugChannelsChannelIdSettingsLayoutRoute: typeof AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren } const AppOrgSlugLayoutRouteChildren: AppOrgSlugLayoutRouteChildren = { - AppOrgSlugMySettingsLayoutRoute: AppOrgSlugMySettingsLayoutRouteWithChildren, - AppOrgSlugNotificationsLayoutRoute: - AppOrgSlugNotificationsLayoutRouteWithChildren, - AppOrgSlugSettingsLayoutRoute: AppOrgSlugSettingsLayoutRouteWithChildren, - AppOrgSlugIndexRoute: AppOrgSlugIndexRoute, - AppOrgSlugChatIdRoute: AppOrgSlugChatIdRouteWithChildren, - AppOrgSlugProfileUserIdRoute: AppOrgSlugProfileUserIdRoute, - AppOrgSlugChatIndexRoute: AppOrgSlugChatIndexRoute, - AppOrgSlugChannelsChannelIdSettingsLayoutRoute: - AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren, + AppOrgSlugMySettingsLayoutRoute: AppOrgSlugMySettingsLayoutRouteWithChildren, + AppOrgSlugNotificationsLayoutRoute: AppOrgSlugNotificationsLayoutRouteWithChildren, + AppOrgSlugSettingsLayoutRoute: AppOrgSlugSettingsLayoutRouteWithChildren, + AppOrgSlugIndexRoute: AppOrgSlugIndexRoute, + AppOrgSlugChatIdRoute: AppOrgSlugChatIdRouteWithChildren, + AppOrgSlugProfileUserIdRoute: AppOrgSlugProfileUserIdRoute, + AppOrgSlugChatIndexRoute: AppOrgSlugChatIndexRoute, + AppOrgSlugChannelsChannelIdSettingsLayoutRoute: + AppOrgSlugChannelsChannelIdSettingsLayoutRouteWithChildren, } -const AppOrgSlugLayoutRouteWithChildren = - AppOrgSlugLayoutRoute._addFileChildren(AppOrgSlugLayoutRouteChildren) +const AppOrgSlugLayoutRouteWithChildren = AppOrgSlugLayoutRoute._addFileChildren( + AppOrgSlugLayoutRouteChildren, +) interface AppLayoutRouteChildren { - AppOrgSlugLayoutRoute: typeof AppOrgSlugLayoutRouteWithChildren - AppIndexRoute: typeof AppIndexRoute - AppOnboardingSetupOrganizationRoute: typeof AppOnboardingSetupOrganizationRoute - AppOnboardingIndexRoute: typeof AppOnboardingIndexRoute - AppSelectOrganizationIndexRoute: typeof AppSelectOrganizationIndexRoute + AppOrgSlugLayoutRoute: typeof AppOrgSlugLayoutRouteWithChildren + AppIndexRoute: typeof AppIndexRoute + AppOnboardingSetupOrganizationRoute: typeof AppOnboardingSetupOrganizationRoute + AppOnboardingIndexRoute: typeof AppOnboardingIndexRoute + AppSelectOrganizationIndexRoute: typeof AppSelectOrganizationIndexRoute } const AppLayoutRouteChildren: AppLayoutRouteChildren = { - AppOrgSlugLayoutRoute: AppOrgSlugLayoutRouteWithChildren, - AppIndexRoute: AppIndexRoute, - AppOnboardingSetupOrganizationRoute: AppOnboardingSetupOrganizationRoute, - AppOnboardingIndexRoute: AppOnboardingIndexRoute, - AppSelectOrganizationIndexRoute: AppSelectOrganizationIndexRoute, + AppOrgSlugLayoutRoute: AppOrgSlugLayoutRouteWithChildren, + AppIndexRoute: AppIndexRoute, + AppOnboardingSetupOrganizationRoute: AppOnboardingSetupOrganizationRoute, + AppOnboardingIndexRoute: AppOnboardingIndexRoute, + AppSelectOrganizationIndexRoute: AppSelectOrganizationIndexRoute, } -const AppLayoutRouteWithChildren = AppLayoutRoute._addFileChildren( - AppLayoutRouteChildren, -) +const AppLayoutRouteWithChildren = AppLayoutRoute._addFileChildren(AppLayoutRouteChildren) interface DevUiLayoutRouteChildren { - DevUiAgentStepsRoute: typeof DevUiAgentStepsRoute + DevUiAgentStepsRoute: typeof DevUiAgentStepsRoute } const DevUiLayoutRouteChildren: DevUiLayoutRouteChildren = { - DevUiAgentStepsRoute: DevUiAgentStepsRoute, + DevUiAgentStepsRoute: DevUiAgentStepsRoute, } -const DevUiLayoutRouteWithChildren = DevUiLayoutRoute._addFileChildren( - DevUiLayoutRouteChildren, -) +const DevUiLayoutRouteWithChildren = DevUiLayoutRoute._addFileChildren(DevUiLayoutRouteChildren) interface DevLayoutRouteChildren { - DevUiLayoutRoute: typeof DevUiLayoutRouteWithChildren + DevUiLayoutRoute: typeof DevUiLayoutRouteWithChildren } const DevLayoutRouteChildren: DevLayoutRouteChildren = { - DevUiLayoutRoute: DevUiLayoutRouteWithChildren, + DevUiLayoutRoute: DevUiLayoutRouteWithChildren, } -const DevLayoutRouteWithChildren = DevLayoutRoute._addFileChildren( - DevLayoutRouteChildren, -) +const DevLayoutRouteWithChildren = DevLayoutRoute._addFileChildren(DevLayoutRouteChildren) const rootRouteChildren: RootRouteChildren = { - AppLayoutRoute: AppLayoutRouteWithChildren, - DevLayoutRoute: DevLayoutRouteWithChildren, - AuthCallbackRoute: AuthCallbackRoute, - AuthDesktopCallbackRoute: AuthDesktopCallbackRoute, - AuthDesktopLoginRoute: AuthDesktopLoginRoute, - AuthLoginRoute: AuthLoginRoute, - JoinSlugRoute: JoinSlugRoute, - DevEmbedsDemoRoute: DevEmbedsDemoRoute, - DevEmbedsGithubRoute: DevEmbedsGithubRoute, - DevEmbedsOpenstatusRoute: DevEmbedsOpenstatusRoute, - DevEmbedsRailwayRoute: DevEmbedsRailwayRoute, - DevEmbedsIndexRoute: DevEmbedsIndexRoute, + AppLayoutRoute: AppLayoutRouteWithChildren, + DevLayoutRoute: DevLayoutRouteWithChildren, + AuthCallbackRoute: AuthCallbackRoute, + AuthDesktopCallbackRoute: AuthDesktopCallbackRoute, + AuthDesktopLoginRoute: AuthDesktopLoginRoute, + AuthLoginRoute: AuthLoginRoute, + JoinSlugRoute: JoinSlugRoute, + DevEmbedsDemoRoute: DevEmbedsDemoRoute, + DevEmbedsGithubRoute: DevEmbedsGithubRoute, + DevEmbedsOpenstatusRoute: DevEmbedsOpenstatusRoute, + DevEmbedsRailwayRoute: DevEmbedsRailwayRoute, + DevEmbedsIndexRoute: DevEmbedsIndexRoute, } -export const routeTree = rootRouteImport - ._addFileChildren(rootRouteChildren) - ._addFileTypes() +export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes() diff --git a/apps/web/src/utils/attachment-url.ts b/apps/web/src/utils/attachment-url.ts index 9b2e24b34..5d1ff16b5 100644 --- a/apps/web/src/utils/attachment-url.ts +++ b/apps/web/src/utils/attachment-url.ts @@ -2,6 +2,5 @@ import type { Attachment } from "@hazel/domain/models" const FALLBACK_PUBLIC_URL = import.meta.env.VITE_R2_PUBLIC_URL || "https://cdn.hazel.sh" -export const getAttachmentUrl = ( - attachment: Pick, -): string => attachment.externalUrl?.trim() || `${FALLBACK_PUBLIC_URL}/${attachment.id}` +export const getAttachmentUrl = (attachment: Pick): string => + attachment.externalUrl?.trim() || `${FALLBACK_PUBLIC_URL}/${attachment.id}` diff --git a/packages/backend-core/src/repositories/attachment-repo.ts b/packages/backend-core/src/repositories/attachment-repo.ts index 30f3a24c6..e49e65564 100644 --- a/packages/backend-core/src/repositories/attachment-repo.ts +++ b/packages/backend-core/src/repositories/attachment-repo.ts @@ -4,10 +4,14 @@ import { ServiceMap, Effect, Layer } from "effect" export class AttachmentRepo extends ServiceMap.Service()("AttachmentRepo", { make: Effect.gen(function* () { - const baseRepo = yield* Repository.makeRepository(schema.attachmentsTable, { insert: Attachment.Insert, update: Attachment.Update }, { - idColumn: "id", - name: "Attachment", - }) + const baseRepo = yield* Repository.makeRepository( + schema.attachmentsTable, + { insert: Attachment.Insert, update: Attachment.Update }, + { + idColumn: "id", + name: "Attachment", + }, + ) return baseRepo }), diff --git a/packages/backend-core/src/repositories/bot-command-repo.ts b/packages/backend-core/src/repositories/bot-command-repo.ts index 4f8b812b0..e50ed0b52 100644 --- a/packages/backend-core/src/repositories/bot-command-repo.ts +++ b/packages/backend-core/src/repositories/bot-command-repo.ts @@ -6,10 +6,14 @@ import { ServiceMap, Effect, Layer, Option } from "effect" export class BotCommandRepo extends ServiceMap.Service()("BotCommandRepo", { make: Effect.gen(function* () { - const baseRepo = yield* Repository.makeRepository(schema.botCommandsTable, { insert: BotCommand.Insert, update: BotCommand.Update }, { - idColumn: "id", - name: "BotCommand", - }) + const baseRepo = yield* Repository.makeRepository( + schema.botCommandsTable, + { insert: BotCommand.Insert, update: BotCommand.Update }, + { + idColumn: "id", + name: "BotCommand", + }, + ) const db = yield* Database.Database // Find all commands for a bot diff --git a/packages/backend-core/src/repositories/bot-repo.ts b/packages/backend-core/src/repositories/bot-repo.ts index d9637ff58..4e15b7deb 100644 --- a/packages/backend-core/src/repositories/bot-repo.ts +++ b/packages/backend-core/src/repositories/bot-repo.ts @@ -1,16 +1,4 @@ -import { - and, - Database, - eq, - ilike, - inArray, - isNull, - Repository, - or, - schema, - sql, - type TxFn, -} from "@hazel/db" +import { and, Database, eq, ilike, inArray, isNull, Repository, or, schema, sql, type TxFn } from "@hazel/db" import type { BotId, UserId } from "@hazel/schema" import { Bot } from "@hazel/domain/models" @@ -18,10 +6,14 @@ import { ServiceMap, Effect, Layer, Option, type Schema } from "effect" export class BotRepo extends ServiceMap.Service()("BotRepo", { make: Effect.gen(function* () { - const baseRepo = yield* Repository.makeRepository(schema.botsTable, { insert: Bot.Insert, update: Bot.Update }, { - idColumn: "id", - name: "Bot", - }) + const baseRepo = yield* Repository.makeRepository( + schema.botsTable, + { insert: Bot.Insert, update: Bot.Update }, + { + idColumn: "id", + name: "Bot", + }, + ) const db = yield* Database.Database // Find bot by ID diff --git a/packages/backend-core/src/repositories/channel-repo.ts b/packages/backend-core/src/repositories/channel-repo.ts index 7eab89ad6..c713b82a2 100644 --- a/packages/backend-core/src/repositories/channel-repo.ts +++ b/packages/backend-core/src/repositories/channel-repo.ts @@ -6,10 +6,14 @@ import { ServiceMap, Effect, Layer, Option } from "effect" export class ChannelRepo extends ServiceMap.Service()("ChannelRepo", { make: Effect.gen(function* () { - const baseRepo = yield* Repository.makeRepository(schema.channelsTable, { insert: Channel.Insert, update: Channel.Update }, { - idColumn: "id", - name: "Channel", - }) + const baseRepo = yield* Repository.makeRepository( + schema.channelsTable, + { insert: Channel.Insert, update: Channel.Update }, + { + idColumn: "id", + name: "Channel", + }, + ) const db = yield* Database.Database const findByOrgAndName = (organizationId: OrganizationId, name: string, tx?: TxFn) => diff --git a/packages/backend-core/src/repositories/custom-emoji-repo.ts b/packages/backend-core/src/repositories/custom-emoji-repo.ts index 7764535b2..92d00946e 100644 --- a/packages/backend-core/src/repositories/custom-emoji-repo.ts +++ b/packages/backend-core/src/repositories/custom-emoji-repo.ts @@ -6,10 +6,14 @@ import { ServiceMap, Effect, Layer, Option } from "effect" export class CustomEmojiRepo extends ServiceMap.Service()("CustomEmojiRepo", { make: Effect.gen(function* () { - const baseRepo = yield* Repository.makeRepository(schema.customEmojisTable, { insert: CustomEmoji.Insert, update: CustomEmoji.Update }, { - idColumn: "id", - name: "CustomEmoji", - }) + const baseRepo = yield* Repository.makeRepository( + schema.customEmojisTable, + { insert: CustomEmoji.Insert, update: CustomEmoji.Update }, + { + idColumn: "id", + name: "CustomEmoji", + }, + ) const db = yield* Database.Database const findByOrgAndName = (organizationId: OrganizationId, name: string) => diff --git a/packages/backend-core/src/repositories/invitation-repo.ts b/packages/backend-core/src/repositories/invitation-repo.ts index 7e1e0e0ee..47ca34915 100644 --- a/packages/backend-core/src/repositories/invitation-repo.ts +++ b/packages/backend-core/src/repositories/invitation-repo.ts @@ -6,10 +6,14 @@ import { ServiceMap, Effect, Layer, Option, type Schema } from "effect" export class InvitationRepo extends ServiceMap.Service()("InvitationRepo", { make: Effect.gen(function* () { - const baseRepo = yield* Repository.makeRepository(schema.invitationsTable, { insert: Invitation.Insert, update: Invitation.Update }, { - idColumn: "id", - name: "Invitation", - }) + const baseRepo = yield* Repository.makeRepository( + schema.invitationsTable, + { insert: Invitation.Insert, update: Invitation.Update }, + { + idColumn: "id", + name: "Invitation", + }, + ) const db = yield* Database.Database const findByWorkosId = (workosInvitationId: WorkOSInvitationId, tx?: TxFn) => diff --git a/packages/backend-core/src/repositories/message-repo.ts b/packages/backend-core/src/repositories/message-repo.ts index a81725aff..4c7ae1380 100644 --- a/packages/backend-core/src/repositories/message-repo.ts +++ b/packages/backend-core/src/repositories/message-repo.ts @@ -37,10 +37,14 @@ export interface ListByChannelParams { export class MessageRepo extends ServiceMap.Service()("MessageRepo", { make: Effect.gen(function* () { - const baseRepo = yield* Repository.makeRepository(schema.messagesTable, { insert: Message.Insert, update: Message.Update }, { - idColumn: "id", - name: "Message", - }) + const baseRepo = yield* Repository.makeRepository( + schema.messagesTable, + { insert: Message.Insert, update: Message.Update }, + { + idColumn: "id", + name: "Message", + }, + ) const db = yield* Database.Database /** diff --git a/packages/backend-core/src/repositories/user-repo.ts b/packages/backend-core/src/repositories/user-repo.ts index 9799226f2..1ba8e32d0 100644 --- a/packages/backend-core/src/repositories/user-repo.ts +++ b/packages/backend-core/src/repositories/user-repo.ts @@ -6,10 +6,14 @@ import { ServiceMap, Effect, Layer, Option, type Schema } from "effect" export class UserRepo extends ServiceMap.Service()("UserRepo", { make: Effect.gen(function* () { - const baseRepo = yield* Repository.makeRepository(schema.usersTable, { insert: User.Insert, update: User.Update }, { - idColumn: "id", - name: "User", - }) + const baseRepo = yield* Repository.makeRepository( + schema.usersTable, + { insert: User.Insert, update: User.Update }, + { + idColumn: "id", + name: "User", + }, + ) const db = yield* Database.Database const findByExternalId = (externalId: string, tx?: TxFn) => diff --git a/packages/db/src/services/repository.ts b/packages/db/src/services/repository.ts index 28f3e5cd9..616f1275e 100644 --- a/packages/db/src/services/repository.ts +++ b/packages/db/src/services/repository.ts @@ -18,7 +18,7 @@ export class EntityNotFound extends Schema.TaggedErrorClass()("E id: Schema.Any, }) {} -export interface RepositorySchemas { +export interface RepositorySchemas> { readonly insert: InsertSchema readonly update: UpdateSchema } @@ -59,7 +59,7 @@ export interface Repository< readonly with: ( id: Id, f: (item: RecordType) => Effect.Effect, - ) => Effect.Effect + ) => Effect.Effect readonly deleteById: ( id: Id, @@ -70,7 +70,7 @@ export interface Repository< export function makeRepository< T extends Table, InsertSchema extends Schema.Top, - UpdateSchema extends Schema.Top, + UpdateSchema extends Schema.Struct, Col extends keyof InferSelectModel & keyof UpdateSchema["Type"] & string, Name extends string, RecordType extends InferSelectModel, @@ -79,13 +79,15 @@ export function makeRepository< table: T, schemas: RepositorySchemas, options: RepositoryOptions, -): Effect.Effect, never, Database> { +): Effect.Effect< + Repository, + never, + Database +> { return Effect.gen(function* () { const db = yield* Database const { idColumn } = options - const updateSchema = ((schemas.update as unknown) as Schema.Struct).mapFields( - Struct.map(Schema.optional), - ) as Schema.Top + const updateSchema = schemas.update.mapFields(Struct.map(Schema.optional)) const insert = (data: InsertSchema["Type"], tx?: TxFn) => pipe( @@ -149,7 +151,7 @@ export function makeRepository< const with_ = ( id: Id, f: (item: RecordType) => Effect.Effect, - ): Effect.Effect => + ): Effect.Effect => pipe( findById(id), Effect.flatMap( @@ -159,7 +161,6 @@ export function makeRepository< }), ), Effect.flatMap(f), - Effect.catchTag("DatabaseError", (err) => Effect.die(err)), ) return { diff --git a/packages/domain/src/models/chat-sync-channel-link-model.ts b/packages/domain/src/models/chat-sync-channel-link-model.ts index c56f4105d..31bf4f22a 100644 --- a/packages/domain/src/models/chat-sync-channel-link-model.ts +++ b/packages/domain/src/models/chat-sync-channel-link-model.ts @@ -15,9 +15,7 @@ export const DiscordWebhookOutboundIdentityConfig = S.Struct({ webhookToken: S.NonEmptyString, defaultAvatarUrl: S.optional(S.NonEmptyString), }) -export type DiscordWebhookOutboundIdentityConfig = S.Schema.Type< - typeof DiscordWebhookOutboundIdentityConfig -> +export type DiscordWebhookOutboundIdentityConfig = S.Schema.Type export const SlackWebhookOutboundIdentityConfig = S.Struct({ kind: S.Literal("slack.webhook"), diff --git a/packages/domain/src/models/integration-connection-model.ts b/packages/domain/src/models/integration-connection-model.ts index edb354dec..74260e290 100644 --- a/packages/domain/src/models/integration-connection-model.ts +++ b/packages/domain/src/models/integration-connection-model.ts @@ -3,14 +3,7 @@ import { Schema as S } from "effect" import * as M from "./utils" import { JsonDate } from "./utils" -export const IntegrationProvider = S.Literals([ - "linear", - "github", - "figma", - "notion", - "discord", - "craft", -]) +export const IntegrationProvider = S.Literals(["linear", "github", "figma", "notion", "discord", "craft"]) export type IntegrationProvider = S.Schema.Type export const ConnectionLevel = S.Literals(["organization", "user"]) diff --git a/packages/domain/src/models/utils.ts b/packages/domain/src/models/utils.ts index aad07c1d6..717942538 100644 --- a/packages/domain/src/models/utils.ts +++ b/packages/domain/src/models/utils.ts @@ -58,9 +58,8 @@ export { export const fields: >(self: A) => A[typeof VariantSchema.TypeId] = VariantSchema.fields -export const structFields = ( - self: A, -): A["fields"] => self.fields +export const structFields = (self: A): A["fields"] => + self.fields export const Override: (value: A) => A & Brand<"Override"> = VariantSchema.Override @@ -370,11 +369,11 @@ export const expose = < model: Model, overrides: Partial> = {}, ): ExposedModel => ({ - Insert: overrides.Insert ?? ((model.insert as unknown) as InsertSchema), - Update: overrides.Update ?? ((model.update as unknown) as UpdateSchema), - Schema: overrides.Schema ?? ((model.json as unknown) as JsonSchema), - Create: overrides.Create ?? ((model.jsonCreate as unknown) as CreateSchema), - Patch: overrides.Patch ?? ((model.jsonUpdate as unknown) as PatchSchema), + Insert: overrides.Insert ?? (model.insert as unknown as InsertSchema), + Update: overrides.Update ?? (model.update as unknown as UpdateSchema), + Schema: overrides.Schema ?? (model.json as unknown as JsonSchema), + Create: overrides.Create ?? (model.jsonCreate as unknown as CreateSchema), + Patch: overrides.Patch ?? (model.jsonUpdate as unknown as PatchSchema), }) export const exposeWithRow = < diff --git a/packages/domain/src/rpc/channel-members.ts b/packages/domain/src/rpc/channel-members.ts index 2b5d18e34..fb72b5e1c 100644 --- a/packages/domain/src/rpc/channel-members.ts +++ b/packages/domain/src/rpc/channel-members.ts @@ -102,10 +102,7 @@ export class ChannelMemberRpcs extends RpcGroup.make( Rpc.make("channelMember.update", { payload: Schema.Struct({ id: ChannelMemberId, - }).pipe( - (s: any) => - Schema.Struct({ ...s.fields, ...(ChannelMember.Patch as any).fields }) as any, - ), + }).pipe(Schema.fieldsAssign((ChannelMember.Patch as Schema.Struct).fields)), success: ChannelMemberResponse, error: Schema.Union([ChannelMemberNotFoundError, UnauthorizedError, InternalServerError]), }) diff --git a/packages/domain/src/rpc/channel-sections.ts b/packages/domain/src/rpc/channel-sections.ts index 26aa36ac1..1a1518620 100644 --- a/packages/domain/src/rpc/channel-sections.ts +++ b/packages/domain/src/rpc/channel-sections.ts @@ -70,10 +70,7 @@ export class ChannelSectionRpcs extends RpcGroup.make( Rpc.make("channelSection.update", { payload: Schema.Struct({ id: ChannelSectionId, - }).pipe( - (s: any) => - Schema.Struct({ ...s.fields, ...(ChannelSection.Patch as any).fields }) as any, - ), + }).pipe(Schema.fieldsAssign((ChannelSection.Patch as Schema.Struct).fields)), success: ChannelSectionResponse, error: Schema.Union([ChannelSectionNotFoundError, UnauthorizedError, InternalServerError]), }) diff --git a/packages/domain/src/rpc/channels.ts b/packages/domain/src/rpc/channels.ts index a3a233a6a..9fd0dda79 100644 --- a/packages/domain/src/rpc/channels.ts +++ b/packages/domain/src/rpc/channels.ts @@ -51,7 +51,7 @@ export class CreateDmChannelRequest extends Schema.Class participantIds: Schema.Array(UserId), type: Schema.Literals(["direct", "single"]), name: Schema.optional(Schema.String), - organizationId: Schema.String.check(Schema.isUUID()), + organizationId: OrganizationId, }) {} /** @@ -111,9 +111,7 @@ export class ChannelRpcs extends RpcGroup.make( Rpc.make("channel.update", { payload: Schema.Struct({ id: ChannelId, - }).pipe( - (s: any) => Schema.Struct({ ...s.fields, ...(Channel.Patch as any).fields }) as any, - ), + }).pipe(Schema.fieldsAssign((Channel.Patch as Schema.Struct).fields)), success: ChannelResponse, error: Schema.Union([ChannelNotFoundError, UnauthorizedError, InternalServerError]), }) diff --git a/packages/domain/src/rpc/messages.ts b/packages/domain/src/rpc/messages.ts index 3dd6416dd..ccc6e3aba 100644 --- a/packages/domain/src/rpc/messages.ts +++ b/packages/domain/src/rpc/messages.ts @@ -97,7 +97,7 @@ export class MessageRpcs extends RpcGroup.make( Rpc.make("message.update", { payload: Schema.Struct({ id: MessageId, - }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(Message.Patch as any).fields }) as any), + }).pipe(Schema.fieldsAssign((Message.Patch as Schema.Struct).fields)), success: MessageResponse, error: Schema.Union([ MessageNotFoundError, diff --git a/packages/domain/src/rpc/organizations.ts b/packages/domain/src/rpc/organizations.ts index 9a35b5096..9fca94552 100644 --- a/packages/domain/src/rpc/organizations.ts +++ b/packages/domain/src/rpc/organizations.ts @@ -80,10 +80,7 @@ export class OrganizationRpcs extends RpcGroup.make( Rpc.make("organization.update", { payload: Schema.Struct({ id: OrganizationId, - }).pipe( - (s: any) => - Schema.Struct({ ...s.fields, ...(Organization.Patch as any).fields }) as any, - ), + }).pipe(Schema.fieldsAssign((Organization.Patch as Schema.Struct).fields)), success: OrganizationResponse, error: Schema.Union([ OrganizationNotFoundError, diff --git a/packages/domain/src/rpc/user-presence-status.ts b/packages/domain/src/rpc/user-presence-status.ts index d58f70687..716676c77 100644 --- a/packages/domain/src/rpc/user-presence-status.ts +++ b/packages/domain/src/rpc/user-presence-status.ts @@ -67,9 +67,7 @@ export class UserPresenceStatusRpcs extends RpcGroup.make( customMessage: Schema.optional(Schema.NullOr(Schema.String)), statusEmoji: Schema.optional(Schema.NullOr(Schema.String)), statusExpiresAt: Schema.optional(Schema.NullOr(JsonDate)), - activeChannelId: Schema.optional( - Schema.NullOr(UserPresenceStatus.Schema.fields.activeChannelId), - ), + activeChannelId: Schema.optional(Schema.NullOr(UserPresenceStatus.Schema.fields.activeChannelId)), suppressNotifications: Schema.optional(Schema.Boolean), }), success: UserPresenceStatusResponse, diff --git a/packages/domain/src/rpc/users.ts b/packages/domain/src/rpc/users.ts index 2bc318883..29dfa4cd5 100644 --- a/packages/domain/src/rpc/users.ts +++ b/packages/domain/src/rpc/users.ts @@ -58,7 +58,7 @@ export class UserRpcs extends RpcGroup.make( Rpc.make("user.update", { payload: Schema.Struct({ id: UserId, - }).pipe((s: any) => Schema.Struct({ ...s.fields, ...(User.Patch as any).fields }) as any), + }).pipe(Schema.fieldsAssign((User.Patch as Schema.Struct).fields)), success: UserResponse, error: Schema.Union([UserNotFoundError, UnauthorizedError, InternalServerError]), }) diff --git a/vitest.config.ts b/vitest.config.ts index 2f23c24e2..ad069f3a8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ projects: [ "packages/*", "apps/backend", + "apps/cluster", "apps/electric-proxy", "apps/link-preview-worker", "apps/web",