khal-os 1.260324.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +23 -0
- package/.genie/mailbox/cli-sent.jsonl +3 -0
- package/.genie/mailbox/ds1-wave2-engineer-1.json +15 -0
- package/.genie/mailbox/ds1-wave2-engineer-2.json +15 -0
- package/.genie/mailbox/ds1-wave2-engineer-3.json +15 -0
- package/.genie/state/os-observability.json +39 -0
- package/.genie/state/tmux-control-mode-terminal.json +28 -0
- package/.genie/wishes/genieos-one-theme/WISH.md +417 -0
- package/.genie/wishes/workos-prod-rbac/WISH.md +345 -0
- package/.github/workflows/ci.yml +39 -0
- package/.github/workflows/release.yml +78 -0
- package/.github/workflows/version.yml +122 -0
- package/.husky/pre-commit +1 -0
- package/.pnpm-approve-builds.json +1 -0
- package/CLAUDE.md +117 -0
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/biome.json +124 -0
- package/bun.lock +1249 -0
- package/docs/workos-setup.md +116 -0
- package/ecosystem.config.cjs +26 -0
- package/instrumentation.ts +8 -0
- package/knip.json +35 -0
- package/nats.conf +7 -0
- package/next.config.ts +25 -0
- package/package.json +78 -0
- package/packages/dev3000-app/components.ts +12 -0
- package/packages/dev3000-app/manifest.ts +19 -0
- package/packages/dev3000-app/package.json +23 -0
- package/packages/dev3000-app/views/dev3000/Dev3000App.tsx +758 -0
- package/packages/dev3000-app/views/dev3000/ErrorsPanel.tsx +160 -0
- package/packages/dev3000-app/views/dev3000/dev3000-context.tsx +21 -0
- package/packages/dev3000-app/views/dev3000/index.ts +4 -0
- package/packages/dev3000-app/views/dev3000/schema.ts +55 -0
- package/packages/dev3000-app/views/dev3000/service/index.ts +358 -0
- package/packages/dev3000-app/views/dev3000/service/runtime +1 -0
- package/packages/dev3000-app/views/dev3000/subjects.ts +9 -0
- package/packages/dev3000-app/views/dev3000/types.ts +77 -0
- package/packages/files-app/components.ts +12 -0
- package/packages/files-app/manifest.ts +19 -0
- package/packages/files-app/package.json +23 -0
- package/packages/files-app/views/files/ContextMenu.tsx +151 -0
- package/packages/files-app/views/files/DeleteConfirmDialog.tsx +39 -0
- package/packages/files-app/views/files/FileItem.tsx +128 -0
- package/packages/files-app/views/files/FilesApp.tsx +509 -0
- package/packages/files-app/views/files/FilesListView.tsx +201 -0
- package/packages/files-app/views/files/FilesToolbar.tsx +117 -0
- package/packages/files-app/views/files/GridView.tsx +90 -0
- package/packages/files-app/views/files/InlineInput.tsx +131 -0
- package/packages/files-app/views/files/UploadOverlay.tsx +61 -0
- package/packages/files-app/views/files/schema.ts +49 -0
- package/packages/files-app/views/files/service/index.ts +184 -0
- package/packages/files-app/views/files/service/runtime +1 -0
- package/packages/files-app/views/files/use-files.ts +201 -0
- package/packages/files-app/views/files/use-upload.ts +105 -0
- package/packages/genie-app/components.ts +12 -0
- package/packages/genie-app/lib/subjects.ts +87 -0
- package/packages/genie-app/manifest.ts +19 -0
- package/packages/genie-app/package.json +29 -0
- package/packages/genie-app/views/genie/service/agent-lifecycle.ts +136 -0
- package/packages/genie-app/views/genie/service/cli.ts +114 -0
- package/packages/genie-app/views/genie/service/comms.ts +141 -0
- package/packages/genie-app/views/genie/service/directory.ts +167 -0
- package/packages/genie-app/views/genie/service/index.ts +219 -0
- package/packages/genie-app/views/genie/service/system.ts +123 -0
- package/packages/genie-app/views/genie/service/teams.ts +191 -0
- package/packages/genie-app/views/genie/service/terminal-proxy.ts +184 -0
- package/packages/genie-app/views/genie/service/tmux-control.ts +318 -0
- package/packages/genie-app/views/genie/service/wishes.ts +270 -0
- package/packages/genie-app/views/genie/ui/GenieApp.tsx +5 -0
- package/packages/genie-app/views/genie/ui/PaneCard.tsx +307 -0
- package/packages/genie-app/views/genie/ui/Sidebar.tsx +212 -0
- package/packages/genie-app/views/genie/ui/TabBar.tsx +70 -0
- package/packages/genie-app/views/genie/ui/WorkspaceCanvas.tsx +343 -0
- package/packages/genie-app/views/genie/ui/XTermPane.tsx +306 -0
- package/packages/genie-app/views/genie/ui/hooks/useNatsAction.ts +54 -0
- package/packages/genie-app/views/genie/ui/hooks/useNatsRequest.ts +68 -0
- package/packages/genie-app/views/genie/ui/panels/AgentsPanel.tsx +399 -0
- package/packages/genie-app/views/genie/ui/panels/ChatPanel.tsx +351 -0
- package/packages/genie-app/views/genie/ui/panels/SystemPanel.tsx +195 -0
- package/packages/genie-app/views/genie/ui/panels/TeamsPanel.tsx +560 -0
- package/packages/genie-app/views/genie/ui/panels/WishesPanel.tsx +424 -0
- package/packages/nats-viewer-app/components.ts +12 -0
- package/packages/nats-viewer-app/manifest.ts +18 -0
- package/packages/nats-viewer-app/package.json +14 -0
- package/packages/nats-viewer-app/views/nats-viewer/ActiveSubs.tsx +34 -0
- package/packages/nats-viewer-app/views/nats-viewer/MessageLog.tsx +247 -0
- package/packages/nats-viewer-app/views/nats-viewer/NatsViewer.tsx +209 -0
- package/packages/nats-viewer-app/views/nats-viewer/PublishPanel.tsx +111 -0
- package/packages/nats-viewer-app/views/nats-viewer/RequestPanel.tsx +165 -0
- package/packages/nats-viewer-app/views/nats-viewer/Sidebar.tsx +59 -0
- package/packages/nats-viewer-app/views/nats-viewer/SubjectCatalog.tsx +63 -0
- package/packages/nats-viewer-app/views/nats-viewer/SubscribeInput.tsx +59 -0
- package/packages/nats-viewer-app/views/nats-viewer/index.ts +5 -0
- package/packages/nats-viewer-app/views/nats-viewer/nats-viewer-context.tsx +31 -0
- package/packages/nats-viewer-app/views/nats-viewer/types.ts +7 -0
- package/packages/nats-viewer-app/views/nats-viewer/use-message-buffer.ts +55 -0
- package/packages/os-cli/package.json +18 -0
- package/packages/os-cli/src/commands/events.ts +176 -0
- package/packages/os-cli/src/commands/logs.ts +96 -0
- package/packages/os-cli/src/commands/status.ts +53 -0
- package/packages/os-cli/src/commands/traces.ts +115 -0
- package/packages/os-cli/src/index.ts +15 -0
- package/packages/os-cli/src/lib/formatter.ts +123 -0
- package/packages/os-cli/src/lib/nats.ts +16 -0
- package/packages/os-cli/src/lib/trace-tree.ts +144 -0
- package/packages/os-cli/tsconfig.json +12 -0
- package/packages/os-sdk/package.json +27 -0
- package/packages/os-sdk/src/api/handler.ts +67 -0
- package/packages/os-sdk/src/config.ts +68 -0
- package/packages/os-sdk/src/db/factory.test.ts +42 -0
- package/packages/os-sdk/src/db/factory.ts +72 -0
- package/packages/os-sdk/src/db/migrate.ts +140 -0
- package/packages/os-sdk/src/db/provision.ts +44 -0
- package/packages/os-sdk/src/index.ts +36 -0
- package/packages/os-sdk/src/service/console-intercept.ts +60 -0
- package/packages/os-sdk/src/service/logger.ts +88 -0
- package/packages/os-sdk/src/service/o11y-streams.ts +88 -0
- package/packages/os-sdk/src/service/runtime.ts +259 -0
- package/packages/os-sdk/src/service/trace.ts +71 -0
- package/packages/os-sdk/tsconfig.json +16 -0
- package/packages/os-ui/package.json +13 -0
- package/packages/os-ui/src/index.ts +29 -0
- package/packages/os-ui/src/server.ts +4 -0
- package/packages/os-ui/tsconfig.json +19 -0
- package/packages/settings-app/components.ts +12 -0
- package/packages/settings-app/manifest.ts +18 -0
- package/packages/settings-app/package.json +14 -0
- package/packages/settings-app/views/settings/Settings.tsx +492 -0
- package/packages/terminal-app/components.ts +12 -0
- package/packages/terminal-app/manifest.ts +20 -0
- package/packages/terminal-app/package.json +23 -0
- package/packages/terminal-app/views/terminal/schema.ts +82 -0
- package/packages/terminal-app/views/terminal/service/index.ts +133 -0
- package/packages/terminal-app/views/terminal/service/runtime +1 -0
- package/packages/terminal-app/views/terminal/service/session.ts +290 -0
- package/packages/terminal-app/views/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
- package/packages/terminal-app/views/terminal/types.ts +26 -0
- package/packages/terminal-app/views/terminal/ui/MultiTerminalApp.tsx +615 -0
- package/packages/terminal-app/views/terminal/ui/SplitDragHandle.tsx +91 -0
- package/packages/terminal-app/views/terminal/ui/SplitPaneRenderer.tsx +112 -0
- package/packages/terminal-app/views/terminal/ui/TerminalPane.tsx +478 -0
- package/packages/terminal-app/views/terminal/ui/TerminalTabBar.tsx +131 -0
- package/pnpm-workspace.yaml +9 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icons/code-server.svg +6 -0
- package/public/icons/default.svg +5 -0
- package/public/icons/dusk/1password.svg +1 -0
- package/public/icons/dusk/activity_monitor.svg +1 -0
- package/public/icons/dusk/app_store.svg +1 -0
- package/public/icons/dusk/atom.svg +1 -0
- package/public/icons/dusk/brave.svg +1 -0
- package/public/icons/dusk/calculator.svg +1 -0
- package/public/icons/dusk/calendar.svg +1 -0
- package/public/icons/dusk/chrome.svg +1 -0
- package/public/icons/dusk/chrome2.svg +1 -0
- package/public/icons/dusk/dashboard.svg +13 -0
- package/public/icons/dusk/discord.svg +1 -0
- package/public/icons/dusk/dropbox.svg +1 -0
- package/public/icons/dusk/electron.svg +1 -0
- package/public/icons/dusk/figma.svg +1 -0
- package/public/icons/dusk/finder.svg +1 -0
- package/public/icons/dusk/finder2.svg +1 -0
- package/public/icons/dusk/finder3.svg +1 -0
- package/public/icons/dusk/firefox.svg +1 -0
- package/public/icons/dusk/framer.svg +1 -0
- package/public/icons/dusk/gimp.svg +1 -0
- package/public/icons/dusk/github_desktop.svg +1 -0
- package/public/icons/dusk/hyper.svg +1 -0
- package/public/icons/dusk/hyper3.svg +1 -0
- package/public/icons/dusk/intellij.svg +1 -0
- package/public/icons/dusk/iterm2.svg +1 -0
- package/public/icons/dusk/itunes.svg +1 -0
- package/public/icons/dusk/mail.svg +1 -0
- package/public/icons/dusk/messenger.svg +1 -0
- package/public/icons/dusk/mongodb.svg +1 -0
- package/public/icons/dusk/notes.svg +1 -0
- package/public/icons/dusk/notion.svg +1 -0
- package/public/icons/dusk/obs.svg +1 -0
- package/public/icons/dusk/pages.svg +1 -0
- package/public/icons/dusk/photos.svg +1 -0
- package/public/icons/dusk/postman.svg +1 -0
- package/public/icons/dusk/preview.svg +1 -0
- package/public/icons/dusk/reminders.svg +1 -0
- package/public/icons/dusk/safari.svg +1 -0
- package/public/icons/dusk/sequel_pro.svg +1 -0
- package/public/icons/dusk/sketch.svg +1 -0
- package/public/icons/dusk/skype.svg +1 -0
- package/public/icons/dusk/slack.svg +1 -0
- package/public/icons/dusk/slack2.svg +1 -0
- package/public/icons/dusk/spotify.svg +1 -0
- package/public/icons/dusk/steam.svg +1 -0
- package/public/icons/dusk/system_preferences.svg +1 -0
- package/public/icons/dusk/tableplus.svg +1 -0
- package/public/icons/dusk/teams.svg +1 -0
- package/public/icons/dusk/telegram.svg +1 -0
- package/public/icons/dusk/terminal.svg +1 -0
- package/public/icons/dusk/todoist.svg +1 -0
- package/public/icons/dusk/trash.svg +1 -0
- package/public/icons/dusk/trello.svg +1 -0
- package/public/icons/dusk/vivaldi.svg +1 -0
- package/public/icons/dusk/vlc.svg +1 -0
- package/public/icons/dusk/vscode.svg +1 -0
- package/public/icons/dusk/whatsapp.svg +1 -0
- package/public/icons/dusk/xeyes.svg +1 -0
- package/public/icons/dusk/zoom.svg +1 -0
- package/public/icons/files.svg +5 -0
- package/public/icons/pwa/icon-192.png +0 -0
- package/public/icons/pwa/icon-512.png +0 -0
- package/public/icons/settings.svg +14 -0
- package/public/icons/terminal.svg +5 -0
- package/public/icons/text-editor.svg +7 -0
- package/public/manifest.json +38 -0
- package/public/next.svg +1 -0
- package/public/sw.js +41 -0
- package/public/vercel.svg +1 -0
- package/public/wallpapers/default.svg +10 -0
- package/public/window.svg +1 -0
- package/scripts/generate-pwa-icons.mjs +33 -0
- package/scripts/install-nats.sh +37 -0
- package/sentry.client.config.ts +21 -0
- package/sentry.edge.config.ts +12 -0
- package/sentry.server.config.ts +12 -0
- package/src/app/api/files/download/route.ts +81 -0
- package/src/app/api/files/download-zip/route.ts +102 -0
- package/src/app/api/files/upload/route.ts +58 -0
- package/src/app/api/webhooks/workos/route.ts +98 -0
- package/src/app/auth/callback/route.ts +16 -0
- package/src/app/auth/logout/route.ts +15 -0
- package/src/app/desktop/desktop-shell.tsx +110 -0
- package/src/app/desktop/layout.tsx +8 -0
- package/src/app/desktop/page.tsx +24 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +7 -0
- package/src/app/layout.tsx +64 -0
- package/src/app/offline/page.tsx +83 -0
- package/src/app/page.tsx +5 -0
- package/src/app/standalone/[appId]/page.tsx +28 -0
- package/src/app/standalone/layout.tsx +10 -0
- package/src/components/app-icon.tsx +55 -0
- package/src/components/apps/_echo/schema.ts +14 -0
- package/src/components/apps/_echo/service/index.ts +42 -0
- package/src/components/apps/app-manifest.ts +97 -0
- package/src/components/apps/app-registry.ts +55 -0
- package/src/components/apps/dev3000/Dev3000App.tsx +224 -0
- package/src/components/apps/dev3000/ErrorsPanel.tsx +160 -0
- package/src/components/apps/dev3000/Sidebar.tsx +41 -0
- package/src/components/apps/dev3000/TimelineLog.tsx +173 -0
- package/src/components/apps/dev3000/dev3000-context.tsx +29 -0
- package/src/components/apps/dev3000/index.ts +4 -0
- package/src/components/apps/dev3000/schema.ts +48 -0
- package/src/components/apps/dev3000/service/index.ts +520 -0
- package/src/components/apps/dev3000/service/runtime +1 -0
- package/src/components/apps/dev3000/types.ts +15 -0
- package/src/components/apps/dev3000/use-message-buffer.ts +46 -0
- package/src/components/apps/files/ContextMenu.tsx +151 -0
- package/src/components/apps/files/DeleteConfirmDialog.tsx +78 -0
- package/src/components/apps/files/FileItem.tsx +128 -0
- package/src/components/apps/files/FilesApp.tsx +509 -0
- package/src/components/apps/files/FilesListView.tsx +201 -0
- package/src/components/apps/files/FilesToolbar.tsx +117 -0
- package/src/components/apps/files/GridView.tsx +90 -0
- package/src/components/apps/files/InlineInput.tsx +131 -0
- package/src/components/apps/files/UploadOverlay.tsx +61 -0
- package/src/components/apps/files/schema.ts +49 -0
- package/src/components/apps/files/service/index.ts +227 -0
- package/src/components/apps/files/service/runtime +1 -0
- package/src/components/apps/files/use-files.ts +201 -0
- package/src/components/apps/files/use-upload.ts +105 -0
- package/src/components/apps/nats-viewer/ActiveSubs.tsx +34 -0
- package/src/components/apps/nats-viewer/MessageLog.tsx +247 -0
- package/src/components/apps/nats-viewer/NatsViewer.tsx +209 -0
- package/src/components/apps/nats-viewer/PublishPanel.tsx +113 -0
- package/src/components/apps/nats-viewer/RequestPanel.tsx +167 -0
- package/src/components/apps/nats-viewer/Sidebar.tsx +62 -0
- package/src/components/apps/nats-viewer/SubjectCatalog.tsx +64 -0
- package/src/components/apps/nats-viewer/SubscribeInput.tsx +59 -0
- package/src/components/apps/nats-viewer/index.ts +5 -0
- package/src/components/apps/nats-viewer/nats-viewer-context.tsx +31 -0
- package/src/components/apps/nats-viewer/types.ts +7 -0
- package/src/components/apps/nats-viewer/use-message-buffer.ts +55 -0
- package/src/components/apps/settings/Settings.tsx +492 -0
- package/src/components/apps/terminal/schema.ts +82 -0
- package/src/components/apps/terminal/service/index.ts +189 -0
- package/src/components/apps/terminal/service/runtime +1 -0
- package/src/components/apps/terminal/service/session.ts +296 -0
- package/src/components/apps/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
- package/src/components/apps/terminal/types.ts +26 -0
- package/src/components/apps/terminal/ui/MultiTerminalApp.tsx +617 -0
- package/src/components/apps/terminal/ui/SplitDragHandle.tsx +91 -0
- package/src/components/apps/terminal/ui/SplitPaneRenderer.tsx +112 -0
- package/src/components/apps/terminal/ui/TerminalPane.tsx +476 -0
- package/src/components/apps/terminal/ui/TerminalTabBar.tsx +131 -0
- package/src/components/desktop/AnimatedBackground.tsx +69 -0
- package/src/components/desktop/Desktop.tsx +79 -0
- package/src/components/desktop/DesktopBackground.tsx +16 -0
- package/src/components/desktop/DesktopIcon.tsx +49 -0
- package/src/components/desktop/ShortcutViewer.tsx +136 -0
- package/src/components/desktop/WindowRenderer.tsx +34 -0
- package/src/components/desktop/WindowSwitcher.tsx +42 -0
- package/src/components/notifications/NotificationCenter.tsx +153 -0
- package/src/components/notifications/NotificationToasts.tsx +66 -0
- package/src/components/notifications/OrphanSessionToast.tsx +293 -0
- package/src/components/os-primitives/collapsible-sidebar.tsx +226 -0
- package/src/components/os-primitives/dialog.tsx +76 -0
- package/src/components/os-primitives/empty-state.tsx +43 -0
- package/src/components/os-primitives/index.ts +21 -0
- package/src/components/os-primitives/list-view.tsx +155 -0
- package/src/components/os-primitives/property-panel.tsx +108 -0
- package/src/components/os-primitives/section-header.tsx +19 -0
- package/src/components/os-primitives/sidebar-nav.tsx +110 -0
- package/src/components/os-primitives/split-pane.tsx +146 -0
- package/src/components/os-primitives/status-badge.tsx +10 -0
- package/src/components/os-primitives/status-bar.tsx +100 -0
- package/src/components/os-primitives/toolbar.tsx +152 -0
- package/src/components/taskbar/AppLauncher.tsx +114 -0
- package/src/components/taskbar/RunningApps.tsx +71 -0
- package/src/components/taskbar/SystemTray.tsx +134 -0
- package/src/components/taskbar/Taskbar.tsx +45 -0
- package/src/components/taskbar/UserMenu.tsx +138 -0
- package/src/components/taskbar/WorkspaceSwitcher.tsx +9 -0
- package/src/components/ui/ContextMenu.tsx +130 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/button.tsx +102 -0
- package/src/components/ui/command.tsx +165 -0
- package/src/components/ui/dropdown-menu.tsx +233 -0
- package/src/components/ui/input.tsx +48 -0
- package/src/components/ui/note.tsx +55 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/spinner.tsx +42 -0
- package/src/components/ui/switch.tsx +36 -0
- package/src/components/ui/theme-provider.tsx +24 -0
- package/src/components/ui/theme-switcher.tsx +51 -0
- package/src/components/ui/tooltip.tsx +62 -0
- package/src/components/window/MobileWindowStack.tsx +218 -0
- package/src/components/window/SnapPreview.tsx +37 -0
- package/src/components/window/StandaloneFrame.tsx +170 -0
- package/src/components/window/Window.tsx +423 -0
- package/src/components/window/WindowContent.tsx +14 -0
- package/src/components/window/WindowControlsOverlay.tsx +89 -0
- package/src/components/window/WindowFrame.tsx +124 -0
- package/src/lib/auth/index.ts +27 -0
- package/src/lib/auth/roles.ts +50 -0
- package/src/lib/auth/types.ts +32 -0
- package/src/lib/auth/use-auth.ts +53 -0
- package/src/lib/auth/webhook-handler.ts +87 -0
- package/src/lib/auth/workos.ts +67 -0
- package/src/lib/constants.ts +1 -0
- package/src/lib/desktop/dedup.ts +57 -0
- package/src/lib/desktop/schema.ts +55 -0
- package/src/lib/files/filename-validation.ts +41 -0
- package/src/lib/files/safe-path.ts +49 -0
- package/src/lib/hooks/use-desktop-nats.ts +438 -0
- package/src/lib/hooks/use-is-mobile.ts +23 -0
- package/src/lib/hooks/use-launch-app.ts +79 -0
- package/src/lib/hooks/use-nats-notifications.ts +84 -0
- package/src/lib/hooks/use-nats.ts +60 -0
- package/src/lib/hooks/use-visual-viewport.ts +72 -0
- package/src/lib/icons/resolve-window-icon.ts +10 -0
- package/src/lib/keyboard/defaults.ts +146 -0
- package/src/lib/keyboard/types.ts +52 -0
- package/src/lib/keyboard/use-global-keybinds.ts +231 -0
- package/src/lib/nats-client.ts +255 -0
- package/src/lib/nats.ts +35 -0
- package/src/lib/notifications/schema.ts +12 -0
- package/src/lib/service-loader.ts +171 -0
- package/src/lib/subjects.ts +64 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/ws-bridge.ts +288 -0
- package/src/lib/ws-protocol.ts +53 -0
- package/src/lib/ws-server.ts +167 -0
- package/src/middleware.ts +57 -0
- package/src/stores/desktop-store.ts +112 -0
- package/src/stores/keybind-store.ts +66 -0
- package/src/stores/notification-store.ts +271 -0
- package/src/stores/theme-store.ts +25 -0
- package/src/stores/window-store.ts +294 -0
- package/src/theme/animations.css +68 -0
- package/src/theme/base.css +123 -0
- package/src/theme/controls.css +35 -0
- package/src/theme/design-tokens.css +276 -0
- package/src/theme/index.css +23 -0
- package/src/theme/menus.css +45 -0
- package/src/theme/status.css +41 -0
- package/src/theme/surfaces.css +94 -0
- package/src/theme/tailwind-map.css +138 -0
- package/src/theme/taskbar.css +25 -0
- package/src/theme/terminal.css +55 -0
- package/src/theme/typography.css +26 -0
- package/src/theme/utilities.css +156 -0
- package/src/theme/window.css +103 -0
- package/src/types/desktop-entry.ts +12 -0
- package/src/types/use-descendants.d.ts +13 -0
- package/src/types/window.ts +28 -0
- package/src/types.d.ts +9 -0
- package/tauri/Cargo.lock +5464 -0
- package/tauri/Cargo.toml +19 -0
- package/tauri/build.rs +3 -0
- package/tauri/capabilities/default.json +36 -0
- package/tauri/icons/128x128.png +0 -0
- package/tauri/icons/128x128@2x.png +0 -0
- package/tauri/icons/32x32.png +0 -0
- package/tauri/icons/icon.png +0 -0
- package/tauri/src/main.rs +396 -0
- package/tauri/tauri.conf.json +23 -0
- package/tsconfig.json +43 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { extractTrace, injectTrace, newSpan } from '@genie-os/sdk/service/trace';
|
|
3
|
+
import type { NatsConnection, Subscription } from '@nats-io/transport-node';
|
|
4
|
+
import { headers } from '@nats-io/transport-node';
|
|
5
|
+
import type { ServerWebSocket } from 'bun';
|
|
6
|
+
import { SUBJECT_PERMISSIONS } from '@/components/apps/app-manifest';
|
|
7
|
+
import type { Session } from './auth/types';
|
|
8
|
+
import { getNatsConnection } from './nats';
|
|
9
|
+
import type { ErrorFrame, ReplyFrame } from './ws-protocol';
|
|
10
|
+
|
|
11
|
+
const MAX_SUBSCRIPTIONS = 100;
|
|
12
|
+
const REQUEST_TIMEOUT_MS = 5000;
|
|
13
|
+
const ALLOWED_PREFIX = 'khal.';
|
|
14
|
+
const MAX_MESSAGES_PER_SECOND = 100;
|
|
15
|
+
const RATE_LIMIT_WINDOW_MS = 1000;
|
|
16
|
+
|
|
17
|
+
export class NatsBridge {
|
|
18
|
+
private subscriptions = new Map<string, Subscription>();
|
|
19
|
+
private nc: NatsConnection | null = null;
|
|
20
|
+
private closed = false;
|
|
21
|
+
private ready: Promise<void>;
|
|
22
|
+
private resolveReady!: () => void;
|
|
23
|
+
private messageCount = 0;
|
|
24
|
+
private rateLimitResetAt = Date.now() + RATE_LIMIT_WINDOW_MS;
|
|
25
|
+
|
|
26
|
+
readonly orgId: string;
|
|
27
|
+
readonly userId: string;
|
|
28
|
+
readonly role: string;
|
|
29
|
+
readonly permissions: string[];
|
|
30
|
+
|
|
31
|
+
constructor(session: Session) {
|
|
32
|
+
this.orgId = session.orgId;
|
|
33
|
+
this.userId = session.userId;
|
|
34
|
+
this.role = session.role;
|
|
35
|
+
this.permissions = session.permissions;
|
|
36
|
+
|
|
37
|
+
this.ready = new Promise<void>((resolve) => {
|
|
38
|
+
this.resolveReady = resolve;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Inject server-verified userId into outbound payloads.
|
|
44
|
+
* Services must read `_authUserId` instead of client-supplied `userId`.
|
|
45
|
+
*/
|
|
46
|
+
private enrichPayload(data: unknown): string {
|
|
47
|
+
if (data === undefined || data === null) {
|
|
48
|
+
return JSON.stringify({ _authUserId: this.userId });
|
|
49
|
+
}
|
|
50
|
+
if (typeof data === 'object' && data !== null) {
|
|
51
|
+
return JSON.stringify({ ...(data as Record<string, unknown>), _authUserId: this.userId });
|
|
52
|
+
}
|
|
53
|
+
// Wrap primitives so _authUserId is always present
|
|
54
|
+
return JSON.stringify({ data, _authUserId: this.userId });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private checkRateLimit(): boolean {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
if (now > this.rateLimitResetAt) {
|
|
60
|
+
this.messageCount = 0;
|
|
61
|
+
this.rateLimitResetAt = now + RATE_LIMIT_WINDOW_MS;
|
|
62
|
+
}
|
|
63
|
+
this.messageCount++;
|
|
64
|
+
return this.messageCount <= MAX_MESSAGES_PER_SECOND;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async init(): Promise<void> {
|
|
68
|
+
this.nc = await getNatsConnection();
|
|
69
|
+
this.resolveReady();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async handleMessage(ws: ServerWebSocket<unknown>, raw: string | Buffer): Promise<void> {
|
|
73
|
+
await this.ready;
|
|
74
|
+
|
|
75
|
+
if (!this.checkRateLimit()) {
|
|
76
|
+
this.sendError(ws, 'rate limit exceeded');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const text = typeof raw === 'string' ? raw : raw.toString();
|
|
81
|
+
|
|
82
|
+
const frame = this.parseFrame(text);
|
|
83
|
+
if (!frame) {
|
|
84
|
+
this.sendError(ws, 'invalid JSON');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!frame.op || typeof frame.op !== 'string') {
|
|
89
|
+
this.sendError(ws, 'missing or invalid "op" field');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (frame.subject && !this.isAllowedSubject(frame.subject)) {
|
|
94
|
+
this.sendError(ws, 'subject not allowed', frame.subject);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await this.dispatch(ws, frame);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private parseFrame(
|
|
102
|
+
text: string
|
|
103
|
+
): { op?: string; subject?: string; data?: unknown; id?: string; _trace?: { traceId?: string } } | null {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(text);
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async dispatch(
|
|
112
|
+
ws: ServerWebSocket<unknown>,
|
|
113
|
+
frame: { op?: string; subject?: string; data?: unknown; id?: string; _trace?: { traceId?: string } }
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
switch (frame.op) {
|
|
116
|
+
case 'sub':
|
|
117
|
+
if (!frame.subject) return this.sendError(ws, 'sub: missing "subject"');
|
|
118
|
+
await this.handleSub(ws, frame.subject);
|
|
119
|
+
break;
|
|
120
|
+
case 'unsub':
|
|
121
|
+
if (!frame.subject) return this.sendError(ws, 'unsub: missing "subject"');
|
|
122
|
+
this.handleUnsub(ws, frame.subject);
|
|
123
|
+
break;
|
|
124
|
+
case 'pub':
|
|
125
|
+
if (!frame.subject) return this.sendError(ws, 'pub: missing "subject"');
|
|
126
|
+
this.handlePub(ws, frame.subject, frame.data, frame._trace);
|
|
127
|
+
break;
|
|
128
|
+
case 'req':
|
|
129
|
+
if (!frame.subject || !frame.id) return this.sendError(ws, 'req: missing "subject" or "id"');
|
|
130
|
+
await this.handleReq(ws, frame.id, frame.subject, frame.data, frame._trace);
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
this.sendError(ws, `unknown op: ${frame.op}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async handleSub(ws: ServerWebSocket<unknown>, subject: string): Promise<void> {
|
|
138
|
+
if (this.subscriptions.has(subject)) {
|
|
139
|
+
this.sendError(ws, `already subscribed to ${subject}`, subject);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (this.subscriptions.size >= MAX_SUBSCRIPTIONS) {
|
|
144
|
+
this.sendError(ws, `max subscriptions (${MAX_SUBSCRIPTIONS}) reached`, subject);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!this.nc) {
|
|
149
|
+
this.sendError(ws, 'NATS not connected', subject);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(`[ws-bridge] sub ${subject} (org=${this.orgId})`);
|
|
154
|
+
const sub = this.nc.subscribe(subject);
|
|
155
|
+
this.subscriptions.set(subject, sub);
|
|
156
|
+
|
|
157
|
+
// Forward messages to WS in background
|
|
158
|
+
(async () => {
|
|
159
|
+
for await (const msg of sub) {
|
|
160
|
+
if (this.closed) break;
|
|
161
|
+
try {
|
|
162
|
+
ws.send(JSON.stringify(this.buildMsgFrame(msg)));
|
|
163
|
+
} catch {
|
|
164
|
+
// WS already closed
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private handleUnsub(ws: ServerWebSocket<unknown>, subject: string): void {
|
|
172
|
+
const sub = this.subscriptions.get(subject);
|
|
173
|
+
if (!sub) {
|
|
174
|
+
this.sendError(ws, `not subscribed to ${subject}`, subject);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
console.log(`[ws-bridge] unsub ${subject} (org=${this.orgId})`);
|
|
178
|
+
sub.unsubscribe();
|
|
179
|
+
this.subscriptions.delete(subject);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Build a WS MsgFrame from a NATS message, enriching with trace context from headers. */
|
|
183
|
+
private buildMsgFrame(
|
|
184
|
+
msg: Parameters<typeof extractTrace>[0] & { data: Uint8Array; subject: string }
|
|
185
|
+
): Record<string, unknown> {
|
|
186
|
+
let data: unknown;
|
|
187
|
+
try {
|
|
188
|
+
data = msg.data.length > 0 ? (msg as { json(): unknown }).json() : null;
|
|
189
|
+
} catch {
|
|
190
|
+
data = (msg as { string(): string }).string();
|
|
191
|
+
}
|
|
192
|
+
const trace = extractTrace(msg);
|
|
193
|
+
const frame: Record<string, unknown> = { subject: msg.subject, data };
|
|
194
|
+
if (trace.traceId) {
|
|
195
|
+
frame._trace = { traceId: trace.traceId, spanId: trace.spanId };
|
|
196
|
+
}
|
|
197
|
+
return frame;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private handlePub(
|
|
201
|
+
_ws: ServerWebSocket<unknown>,
|
|
202
|
+
subject: string,
|
|
203
|
+
data: unknown,
|
|
204
|
+
wsTrace?: { traceId?: string }
|
|
205
|
+
): void {
|
|
206
|
+
if (!this.nc) return;
|
|
207
|
+
console.log(`[ws-bridge] pub ${subject} (org=${this.orgId})`);
|
|
208
|
+
const trace = newSpan(wsTrace?.traceId);
|
|
209
|
+
const hdrs = injectTrace(headers(), trace);
|
|
210
|
+
this.nc.publish(subject, this.enrichPayload(data), { headers: hdrs });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async handleReq(
|
|
214
|
+
ws: ServerWebSocket<unknown>,
|
|
215
|
+
id: string,
|
|
216
|
+
subject: string,
|
|
217
|
+
data: unknown,
|
|
218
|
+
wsTrace?: { traceId?: string }
|
|
219
|
+
): Promise<void> {
|
|
220
|
+
if (!this.nc) {
|
|
221
|
+
this.sendError(ws, 'NATS not connected', subject);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
console.log(`[ws-bridge] req ${subject} id=${id} (org=${this.orgId})`);
|
|
225
|
+
const trace = newSpan(wsTrace?.traceId);
|
|
226
|
+
const hdrs = injectTrace(headers(), trace);
|
|
227
|
+
try {
|
|
228
|
+
const reply = await this.nc.request(subject, this.enrichPayload(data), {
|
|
229
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
230
|
+
headers: hdrs,
|
|
231
|
+
});
|
|
232
|
+
let replyData: unknown;
|
|
233
|
+
try {
|
|
234
|
+
replyData = reply.data.length > 0 ? reply.json() : null;
|
|
235
|
+
} catch {
|
|
236
|
+
replyData = reply.string();
|
|
237
|
+
}
|
|
238
|
+
// Propagate trace context back to the browser
|
|
239
|
+
const replyTrace = extractTrace(reply);
|
|
240
|
+
const frame: ReplyFrame & { _trace?: { traceId: string; spanId?: string } } = { id, data: replyData };
|
|
241
|
+
frame._trace = {
|
|
242
|
+
traceId: replyTrace.traceId || trace.traceId,
|
|
243
|
+
spanId: replyTrace.spanId || trace.spanId,
|
|
244
|
+
};
|
|
245
|
+
ws.send(JSON.stringify(frame));
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
248
|
+
this.sendError(ws, `req failed: ${message}`, subject);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async cleanup(): Promise<void> {
|
|
253
|
+
this.closed = true;
|
|
254
|
+
console.log(`[ws-bridge] cleanup: ${this.subscriptions.size} subscription(s)`);
|
|
255
|
+
for (const [subject, sub] of this.subscriptions) {
|
|
256
|
+
console.log(`[ws-bridge] unsubscribing ${subject}`);
|
|
257
|
+
sub.unsubscribe();
|
|
258
|
+
}
|
|
259
|
+
this.subscriptions.clear();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private isAllowedSubject(subject: string): boolean {
|
|
263
|
+
if (!subject.startsWith(ALLOWED_PREFIX)) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
// Enforce org-scoped access: subject must be khal.<orgId>.*
|
|
267
|
+
const orgPrefix = `khal.${this.orgId}.`;
|
|
268
|
+
if (!subject.startsWith(orgPrefix)) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
// Enforce app-level permissions (defined per-app in subjects.ts)
|
|
272
|
+
const segment = subject.slice(orgPrefix.length).split('.')[0];
|
|
273
|
+
const requiredPermission = SUBJECT_PERMISSIONS[segment];
|
|
274
|
+
if (requiredPermission && !this.permissions.includes(requiredPermission)) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private sendError(ws: ServerWebSocket<unknown>, error: string, subject?: string): void {
|
|
281
|
+
const frame: ErrorFrame = { error, ...(subject ? { subject } : {}) };
|
|
282
|
+
try {
|
|
283
|
+
ws.send(JSON.stringify(frame));
|
|
284
|
+
} catch {
|
|
285
|
+
// WS already closed
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Static, Type } from '@sinclair/typebox';
|
|
2
|
+
|
|
3
|
+
// Client -> Server frames
|
|
4
|
+
|
|
5
|
+
export const SubFrame = Type.Object({
|
|
6
|
+
op: Type.Literal('sub'),
|
|
7
|
+
subject: Type.String(),
|
|
8
|
+
});
|
|
9
|
+
export type SubFrame = Static<typeof SubFrame>;
|
|
10
|
+
|
|
11
|
+
export const UnsubFrame = Type.Object({
|
|
12
|
+
op: Type.Literal('unsub'),
|
|
13
|
+
subject: Type.String(),
|
|
14
|
+
});
|
|
15
|
+
export type UnsubFrame = Static<typeof UnsubFrame>;
|
|
16
|
+
|
|
17
|
+
export const PubFrame = Type.Object({
|
|
18
|
+
op: Type.Literal('pub'),
|
|
19
|
+
subject: Type.String(),
|
|
20
|
+
data: Type.Optional(Type.Unknown()),
|
|
21
|
+
});
|
|
22
|
+
export type PubFrame = Static<typeof PubFrame>;
|
|
23
|
+
|
|
24
|
+
export const ReqFrame = Type.Object({
|
|
25
|
+
op: Type.Literal('req'),
|
|
26
|
+
id: Type.String(),
|
|
27
|
+
subject: Type.String(),
|
|
28
|
+
data: Type.Optional(Type.Unknown()),
|
|
29
|
+
});
|
|
30
|
+
export type ReqFrame = Static<typeof ReqFrame>;
|
|
31
|
+
|
|
32
|
+
export const ClientFrame = Type.Union([SubFrame, UnsubFrame, PubFrame, ReqFrame]);
|
|
33
|
+
export type ClientFrame = Static<typeof ClientFrame>;
|
|
34
|
+
|
|
35
|
+
// Server -> Client frames
|
|
36
|
+
|
|
37
|
+
export const MsgFrame = Type.Object({
|
|
38
|
+
subject: Type.String(),
|
|
39
|
+
data: Type.Unknown(),
|
|
40
|
+
});
|
|
41
|
+
export type MsgFrame = Static<typeof MsgFrame>;
|
|
42
|
+
|
|
43
|
+
export const ReplyFrame = Type.Object({
|
|
44
|
+
id: Type.String(),
|
|
45
|
+
data: Type.Unknown(),
|
|
46
|
+
});
|
|
47
|
+
export type ReplyFrame = Static<typeof ReplyFrame>;
|
|
48
|
+
|
|
49
|
+
export const ErrorFrame = Type.Object({
|
|
50
|
+
error: Type.String(),
|
|
51
|
+
subject: Type.Optional(Type.String()),
|
|
52
|
+
});
|
|
53
|
+
export type ErrorFrame = Static<typeof ErrorFrame>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import type { ServerWebSocket } from 'bun';
|
|
3
|
+
import { unsealData } from 'iron-session';
|
|
4
|
+
import { decodeJwt } from 'jose';
|
|
5
|
+
import { DEFAULT_ROLE_PERMISSIONS } from '@/components/apps/app-manifest';
|
|
6
|
+
import { normalizeRole } from './auth/roles';
|
|
7
|
+
import type { Session } from './auth/types';
|
|
8
|
+
import { NatsBridge } from './ws-bridge';
|
|
9
|
+
|
|
10
|
+
const PORT = Number(process.env.WS_BRIDGE_PORT) || 4280;
|
|
11
|
+
const COOKIE_NAME = process.env.WORKOS_COOKIE_NAME || 'wos-session';
|
|
12
|
+
const COOKIE_PASSWORD = process.env.WORKOS_COOKIE_PASSWORD || '';
|
|
13
|
+
|
|
14
|
+
const INSTANCE_ID = process.env.KHAL_INSTANCE_ID;
|
|
15
|
+
if (!INSTANCE_ID || INSTANCE_ID.trim() === '') {
|
|
16
|
+
console.error('[ws-server] FATAL: KHAL_INSTANCE_ID env var is required and must be non-empty');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface WsData {
|
|
21
|
+
session: Session;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const bridges = new WeakMap<ServerWebSocket<WsData>, NatsBridge>();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse a specific cookie value from a Cookie header string.
|
|
28
|
+
*/
|
|
29
|
+
function parseCookie(cookieHeader: string, name: string): string | undefined {
|
|
30
|
+
const cookies = cookieHeader.split(';');
|
|
31
|
+
for (const cookie of cookies) {
|
|
32
|
+
const [key, ...rest] = cookie.split('=');
|
|
33
|
+
if (key.trim() === name) {
|
|
34
|
+
return rest.join('=').trim();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Verify the WorkOS AuthKit session cookie and extract session data.
|
|
42
|
+
*
|
|
43
|
+
* The cookie is sealed with iron-session using WORKOS_COOKIE_PASSWORD.
|
|
44
|
+
* Inside is a WorkOS Session containing an accessToken JWT with org_id, role, and permissions.
|
|
45
|
+
*/
|
|
46
|
+
async function verifySessionCookie(cookieHeader: string | null): Promise<Session | null> {
|
|
47
|
+
if (!cookieHeader) return null;
|
|
48
|
+
if (!COOKIE_PASSWORD) {
|
|
49
|
+
console.error('[ws-server] WORKOS_COOKIE_PASSWORD not set, cannot verify session');
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sealedValue = parseCookie(cookieHeader, COOKIE_NAME);
|
|
54
|
+
if (!sealedValue) return null;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Unseal the iron-session encrypted cookie
|
|
58
|
+
const workosSession = await unsealData<{ accessToken: string; refreshToken: string }>(sealedValue, {
|
|
59
|
+
password: COOKIE_PASSWORD,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!workosSession.accessToken) return null;
|
|
63
|
+
|
|
64
|
+
// Decode the JWT to extract org_id, role, permissions
|
|
65
|
+
// Note: We decode (not verify) here. The cookie was already authenticated
|
|
66
|
+
// via iron-session's encryption — the JWT signature provides additional
|
|
67
|
+
// integrity but the HMAC seal is the primary trust boundary.
|
|
68
|
+
const claims = decodeJwt<{
|
|
69
|
+
sub?: string;
|
|
70
|
+
org_id?: string;
|
|
71
|
+
role?: string;
|
|
72
|
+
permissions?: string[];
|
|
73
|
+
exp?: number;
|
|
74
|
+
}>(workosSession.accessToken);
|
|
75
|
+
|
|
76
|
+
// Reject tokens without exp claim or expired tokens
|
|
77
|
+
if (!claims.exp || claims.exp < Date.now() / 1000) {
|
|
78
|
+
console.warn('[ws-server] session token expired or missing exp claim');
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const userId = claims.sub ?? '';
|
|
83
|
+
const orgId = INSTANCE_ID;
|
|
84
|
+
|
|
85
|
+
if (!userId) {
|
|
86
|
+
console.warn('[ws-server] session cookie missing userId');
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const effectiveRole = normalizeRole(claims.role);
|
|
91
|
+
const effectivePermissions =
|
|
92
|
+
claims.permissions && claims.permissions.length > 0
|
|
93
|
+
? claims.permissions
|
|
94
|
+
: (DEFAULT_ROLE_PERMISSIONS[effectiveRole] ?? []);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
userId,
|
|
98
|
+
orgId,
|
|
99
|
+
role: effectiveRole,
|
|
100
|
+
permissions: effectivePermissions,
|
|
101
|
+
};
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.warn('[ws-server] failed to verify session cookie:', err);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const server = Bun.serve<WsData>({
|
|
109
|
+
port: PORT,
|
|
110
|
+
async fetch(req, server) {
|
|
111
|
+
const cookieHeader = req.headers.get('cookie');
|
|
112
|
+
let session = await verifySessionCookie(cookieHeader);
|
|
113
|
+
|
|
114
|
+
// Bypass auth for d3k's headless Chrome when OS_SECRET is configured
|
|
115
|
+
if (!session && process.env.OS_SECRET) {
|
|
116
|
+
const ua = req.headers.get('user-agent') ?? '';
|
|
117
|
+
if (ua.includes('HeadlessChrome')) {
|
|
118
|
+
session = {
|
|
119
|
+
userId: 'machine',
|
|
120
|
+
orgId: INSTANCE_ID,
|
|
121
|
+
role: 'platform-owner',
|
|
122
|
+
permissions: DEFAULT_ROLE_PERMISSIONS['platform-owner'] ?? [],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!session) {
|
|
128
|
+
console.log('[ws-server] rejected WS upgrade: invalid or missing session cookie');
|
|
129
|
+
return new Response(null, { status: 401 });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (server.upgrade(req, { data: { session } })) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return new Response('khal ws-bridge', { status: 200 });
|
|
137
|
+
},
|
|
138
|
+
websocket: {
|
|
139
|
+
async open(ws: ServerWebSocket<WsData>) {
|
|
140
|
+
const { session } = ws.data;
|
|
141
|
+
console.log(`[ws-server] client connected (org=${session.orgId} user=${session.userId})`);
|
|
142
|
+
const bridge = new NatsBridge(session);
|
|
143
|
+
bridges.set(ws, bridge);
|
|
144
|
+
try {
|
|
145
|
+
await bridge.init();
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error('[ws-server] failed to init bridge:', err);
|
|
148
|
+
ws.close(1011, 'NATS connection failed');
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
async message(ws: ServerWebSocket<WsData>, message: string | Buffer) {
|
|
152
|
+
const bridge = bridges.get(ws);
|
|
153
|
+
if (!bridge) return;
|
|
154
|
+
await bridge.handleMessage(ws, message);
|
|
155
|
+
},
|
|
156
|
+
async close(ws: ServerWebSocket<WsData>) {
|
|
157
|
+
console.log('[ws-server] client disconnected');
|
|
158
|
+
const bridge = bridges.get(ws);
|
|
159
|
+
if (bridge) {
|
|
160
|
+
await bridge.cleanup();
|
|
161
|
+
bridges.delete(ws);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
console.log(`[ws-server] listening on port ${server.port}`);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
|
|
2
|
+
import type { NextFetchEvent, NextRequest } from 'next/server';
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AuthKit middleware — protects /desktop routes and allows public paths.
|
|
7
|
+
*
|
|
8
|
+
* When middlewareAuth.enabled is true, all routes are protected by default
|
|
9
|
+
* except those listed in unauthenticatedPaths. Unauthenticated requests
|
|
10
|
+
* to protected routes are redirected to the AuthKit hosted login page.
|
|
11
|
+
*
|
|
12
|
+
* Headless Chrome bypass: when OS_SECRET is set, requests from d3k's
|
|
13
|
+
* headless Chrome skip auth so the desktop renders for monitoring.
|
|
14
|
+
*/
|
|
15
|
+
const workosAuth = authkitMiddleware({
|
|
16
|
+
middlewareAuth: {
|
|
17
|
+
enabled: true,
|
|
18
|
+
unauthenticatedPaths: [
|
|
19
|
+
'/',
|
|
20
|
+
'/offline',
|
|
21
|
+
'/auth/callback',
|
|
22
|
+
'/auth/:path*',
|
|
23
|
+
'/standalone/:path*',
|
|
24
|
+
'/api/webhooks/:path*',
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export default async function middleware(req: NextRequest, event: NextFetchEvent) {
|
|
30
|
+
const ua = req.headers.get('user-agent') ?? '';
|
|
31
|
+
if (process.env.OS_SECRET && ua.includes('HeadlessChrome')) {
|
|
32
|
+
const cookieName = process.env.WORKOS_COOKIE_NAME || 'wos-session';
|
|
33
|
+
if (!req.cookies.has(cookieName)) {
|
|
34
|
+
// No session cookie — d3k monitoring mode, bypass auth entirely
|
|
35
|
+
const headers = new Headers(req.headers);
|
|
36
|
+
headers.set('x-workos-middleware', 'true');
|
|
37
|
+
headers.set('x-url', req.url);
|
|
38
|
+
return NextResponse.next({ request: { headers } });
|
|
39
|
+
}
|
|
40
|
+
// Has session cookie — let authkitMiddleware process it normally
|
|
41
|
+
// (authenticated agent-browser sessions should get full user context)
|
|
42
|
+
}
|
|
43
|
+
return workosAuth(req, event);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const config = {
|
|
47
|
+
matcher: [
|
|
48
|
+
/*
|
|
49
|
+
* Match all request paths except:
|
|
50
|
+
* - _next/static (static files)
|
|
51
|
+
* - _next/image (image optimization files)
|
|
52
|
+
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
|
|
53
|
+
* - public assets
|
|
54
|
+
*/
|
|
55
|
+
'/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)',
|
|
56
|
+
],
|
|
57
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
import type { AppId } from '@/components/apps/app-registry';
|
|
4
|
+
import { APP_REGISTRY } from '@/components/apps/app-registry';
|
|
5
|
+
import type { DesktopEntry } from '@/types/desktop-entry';
|
|
6
|
+
|
|
7
|
+
const BUILTIN_APPS: DesktopEntry[] = [
|
|
8
|
+
{
|
|
9
|
+
id: 'terminal',
|
|
10
|
+
name: 'Terminal',
|
|
11
|
+
icon: '/icons/dusk/terminal.svg',
|
|
12
|
+
exec: null,
|
|
13
|
+
type: 'builtin',
|
|
14
|
+
component: 'terminal',
|
|
15
|
+
categories: ['System'],
|
|
16
|
+
comment: 'Terminal emulator',
|
|
17
|
+
onDesktop: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'files',
|
|
21
|
+
name: 'Files',
|
|
22
|
+
icon: '/icons/dusk/finder.svg',
|
|
23
|
+
exec: null,
|
|
24
|
+
type: 'builtin',
|
|
25
|
+
component: 'files',
|
|
26
|
+
categories: ['System'],
|
|
27
|
+
comment: 'File browser',
|
|
28
|
+
onDesktop: true,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'settings',
|
|
32
|
+
name: 'Settings',
|
|
33
|
+
icon: '/icons/dusk/system_preferences.svg',
|
|
34
|
+
exec: null,
|
|
35
|
+
type: 'builtin',
|
|
36
|
+
component: 'settings',
|
|
37
|
+
categories: ['System'],
|
|
38
|
+
comment: 'Desktop settings',
|
|
39
|
+
onDesktop: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'nats-viewer',
|
|
43
|
+
name: 'NATS Viewer',
|
|
44
|
+
icon: '/icons/dusk/activity_monitor.svg',
|
|
45
|
+
exec: null,
|
|
46
|
+
type: 'builtin',
|
|
47
|
+
component: 'nats-viewer',
|
|
48
|
+
categories: ['Development'],
|
|
49
|
+
comment: 'NATS message viewer and debugger',
|
|
50
|
+
onDesktop: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'dev3000',
|
|
54
|
+
name: 'dev3000',
|
|
55
|
+
icon: '/icons/dusk/dashboard.svg',
|
|
56
|
+
exec: null,
|
|
57
|
+
type: 'builtin',
|
|
58
|
+
component: 'dev3000',
|
|
59
|
+
categories: ['Development'],
|
|
60
|
+
comment: 'dev3000 unified development logs',
|
|
61
|
+
onDesktop: true,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
interface DesktopStore {
|
|
66
|
+
apps: DesktopEntry[];
|
|
67
|
+
desktopIcons: DesktopEntry[];
|
|
68
|
+
wallpaper: string;
|
|
69
|
+
pinnedApps: string[];
|
|
70
|
+
|
|
71
|
+
setApps: (apps: DesktopEntry[]) => void;
|
|
72
|
+
setWallpaper: (url: string) => void;
|
|
73
|
+
pinApp: (appId: string) => void;
|
|
74
|
+
unpinApp: (appId: string) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const useDesktopStore = create<DesktopStore>((set) => ({
|
|
78
|
+
apps: BUILTIN_APPS,
|
|
79
|
+
desktopIcons: BUILTIN_APPS,
|
|
80
|
+
wallpaper: '/wallpapers/default.svg',
|
|
81
|
+
pinnedApps: [],
|
|
82
|
+
|
|
83
|
+
setApps: (apps) => set({ apps }),
|
|
84
|
+
setWallpaper: (url) => set({ wallpaper: url }),
|
|
85
|
+
pinApp: (appId) =>
|
|
86
|
+
set((state) => ({
|
|
87
|
+
pinnedApps: state.pinnedApps.includes(appId) ? state.pinnedApps : [...state.pinnedApps, appId],
|
|
88
|
+
})),
|
|
89
|
+
unpinApp: (appId) =>
|
|
90
|
+
set((state) => ({
|
|
91
|
+
pinnedApps: state.pinnedApps.filter((id) => id !== appId),
|
|
92
|
+
})),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns desktop apps filtered by the given permissions array.
|
|
97
|
+
* An app is included if:
|
|
98
|
+
* - It has no requiredPermission in the registry, OR
|
|
99
|
+
* - Its requiredPermission is in the user's permissions list
|
|
100
|
+
*/
|
|
101
|
+
export function useFilteredDesktopApps(permissions: string[]): DesktopEntry[] {
|
|
102
|
+
const desktopIcons = useDesktopStore((s) => s.desktopIcons);
|
|
103
|
+
|
|
104
|
+
return useMemo(() => {
|
|
105
|
+
return desktopIcons.filter((entry) => {
|
|
106
|
+
const appId = entry.component ?? entry.id;
|
|
107
|
+
const registryEntry = APP_REGISTRY[appId as AppId];
|
|
108
|
+
if (!registryEntry?.requiredPermission) return true;
|
|
109
|
+
return permissions.includes(registryEntry.requiredPermission);
|
|
110
|
+
});
|
|
111
|
+
}, [desktopIcons, permissions]);
|
|
112
|
+
}
|