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 @@
|
|
|
1
|
+
<svg width="512" height="512" fill="none" xmlns="http://www.w3.org/2000/svg"><circle fill="#1E1E1E" cx="256" cy="256" r="256"/><path d="M151 191h107.35a50 50 0 0 1 50 50v70a10 10 0 0 1-10 10H191a50 50 0 0 1-50-50v-70a10 10 0 0 1 10-10zm162.9 40.12s50.86-40.2 52.94-40.12h.2a4 4 0 0 1 4 4v122a4 4 0 0 1-4 4l-53.14-40.12z" fill="#fff"/></svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
|
2
|
+
<rect width="48" height="48" rx="10" fill="#111111" stroke="#222222" stroke-width="1"/>
|
|
3
|
+
<path d="M13 16h7l2.5 2.5H35a1.5 1.5 0 011.5 1.5v14a1.5 1.5 0 01-1.5 1.5H13a1.5 1.5 0 01-1.5-1.5V17.5A1.5 1.5 0 0113 16z" stroke="#ffffff" stroke-width="1.5" fill="none"/>
|
|
4
|
+
<path d="M11.5 21.5H36.5" stroke="#333333" stroke-width="1"/>
|
|
5
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
|
2
|
+
<rect width="48" height="48" rx="10" fill="#111111" stroke="#222222" stroke-width="1"/>
|
|
3
|
+
<circle cx="24" cy="24" r="6" stroke="#ffffff" stroke-width="1.5" fill="none"/>
|
|
4
|
+
<g stroke="#ffffff" stroke-width="1.5" stroke-linecap="round">
|
|
5
|
+
<line x1="24" y1="10" x2="24" y2="14"/>
|
|
6
|
+
<line x1="24" y1="34" x2="24" y2="38"/>
|
|
7
|
+
<line x1="10" y1="24" x2="14" y2="24"/>
|
|
8
|
+
<line x1="34" y1="24" x2="38" y2="24"/>
|
|
9
|
+
<line x1="14.1" y1="14.1" x2="16.9" y2="16.9"/>
|
|
10
|
+
<line x1="31.1" y1="31.1" x2="33.9" y2="33.9"/>
|
|
11
|
+
<line x1="14.1" y1="33.9" x2="16.9" y2="31.1"/>
|
|
12
|
+
<line x1="31.1" y1="16.9" x2="33.9" y2="14.1"/>
|
|
13
|
+
</g>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
|
2
|
+
<rect width="48" height="48" rx="10" fill="#111111" stroke="#222222" stroke-width="1"/>
|
|
3
|
+
<path d="M15 17l7 7-7 7" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<line x1="25" y1="31" x2="33" y2="31" stroke="#666666" stroke-width="2" stroke-linecap="round"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
|
2
|
+
<rect width="48" height="48" rx="10" fill="#111111" stroke="#222222" stroke-width="1"/>
|
|
3
|
+
<rect x="12" y="10" width="24" height="28" rx="2" stroke="#ffffff" stroke-width="1.5" fill="none"/>
|
|
4
|
+
<line x1="17" y1="18" x2="31" y2="18" stroke="#666666" stroke-width="1.5" stroke-linecap="round"/>
|
|
5
|
+
<line x1="17" y1="23" x2="28" y2="23" stroke="#666666" stroke-width="1.5" stroke-linecap="round"/>
|
|
6
|
+
<line x1="17" y1="28" x2="24" y2="28" stroke="#666666" stroke-width="1.5" stroke-linecap="round"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Khal",
|
|
3
|
+
"short_name": "Khal",
|
|
4
|
+
"description": "Desktop-in-browser OS shell",
|
|
5
|
+
"start_url": "/desktop",
|
|
6
|
+
"scope": "/",
|
|
7
|
+
"display": "standalone",
|
|
8
|
+
"background_color": "#0a0f0a",
|
|
9
|
+
"theme_color": "#39ff14",
|
|
10
|
+
"orientation": "any",
|
|
11
|
+
"icons": [
|
|
12
|
+
{
|
|
13
|
+
"src": "/icons/pwa/icon-192.png",
|
|
14
|
+
"sizes": "192x192",
|
|
15
|
+
"type": "image/png"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"src": "/icons/pwa/icon-512.png",
|
|
19
|
+
"sizes": "512x512",
|
|
20
|
+
"type": "image/png",
|
|
21
|
+
"purpose": "any maskable"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"shortcuts": [
|
|
25
|
+
{
|
|
26
|
+
"name": "Terminal",
|
|
27
|
+
"short_name": "Terminal",
|
|
28
|
+
"url": "/desktop",
|
|
29
|
+
"description": "Open a terminal window"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "Dashboard",
|
|
33
|
+
"short_name": "Dashboard",
|
|
34
|
+
"url": "/desktop",
|
|
35
|
+
"description": "Open the router dashboard"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
package/public/next.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
package/public/sw.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const CACHE_NAME = 'khal-v1';
|
|
2
|
+
const PRECACHE = ['/', '/desktop', '/offline'];
|
|
3
|
+
|
|
4
|
+
self.addEventListener('install', (event) => {
|
|
5
|
+
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE)));
|
|
6
|
+
self.skipWaiting();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
self.addEventListener('activate', (event) => {
|
|
10
|
+
event.waitUntil(
|
|
11
|
+
caches
|
|
12
|
+
.keys()
|
|
13
|
+
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))))
|
|
14
|
+
.then(() => self.clients.claim())
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
self.addEventListener('fetch', (event) => {
|
|
19
|
+
const { request } = event;
|
|
20
|
+
// Only handle GET
|
|
21
|
+
if (request.method !== 'GET') return;
|
|
22
|
+
// API routes: network only
|
|
23
|
+
const url = new URL(request.url);
|
|
24
|
+
if (url.pathname.startsWith('/api/')) return;
|
|
25
|
+
|
|
26
|
+
event.respondWith(
|
|
27
|
+
caches.match(request).then((cached) => {
|
|
28
|
+
if (cached) return cached;
|
|
29
|
+
return fetch(request)
|
|
30
|
+
.then((response) => {
|
|
31
|
+
// Only cache same-origin successful responses
|
|
32
|
+
if (response.ok && response.type === 'basic') {
|
|
33
|
+
const clone = response.clone();
|
|
34
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
|
35
|
+
}
|
|
36
|
+
return response;
|
|
37
|
+
})
|
|
38
|
+
.catch(() => caches.match('/offline'));
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080">
|
|
2
|
+
<rect width="1920" height="1080" fill="#000000"/>
|
|
3
|
+
<defs>
|
|
4
|
+
<radialGradient id="g1" cx="30%" cy="40%" r="60%">
|
|
5
|
+
<stop offset="0%" stop-color="#111111"/>
|
|
6
|
+
<stop offset="100%" stop-color="#000000"/>
|
|
7
|
+
</radialGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
<rect width="1920" height="1080" fill="url(#g1)"/>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdirSync } from 'fs';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const _require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
// Resolve sharp from the pnpm virtual store (bundled with Next.js)
|
|
10
|
+
const sharpPath = join(__dirname, '../node_modules/.pnpm/sharp@0.34.5/node_modules/sharp/lib/index.js');
|
|
11
|
+
const sharp = (await import(sharpPath)).default;
|
|
12
|
+
|
|
13
|
+
const outDir = join(__dirname, '../public/icons/pwa');
|
|
14
|
+
mkdirSync(outDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
function makeSvg(size) {
|
|
17
|
+
const pad = size * 0.2;
|
|
18
|
+
const iconSize = size - pad * 2;
|
|
19
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}">
|
|
20
|
+
<rect width="${size}" height="${size}" rx="${size * 0.2}" fill="#0a0f0a"/>
|
|
21
|
+
<g transform="translate(${pad}, ${pad}) scale(${iconSize / 24})">
|
|
22
|
+
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"
|
|
23
|
+
fill="#39ff14" stroke="#39ff14" stroke-width="0.5"/>
|
|
24
|
+
</g>
|
|
25
|
+
</svg>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const size of [192, 512]) {
|
|
29
|
+
await sharp(Buffer.from(makeSvg(size)))
|
|
30
|
+
.png()
|
|
31
|
+
.toFile(join(outDir, `icon-${size}.png`));
|
|
32
|
+
console.log(`icon-${size}.png`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Install NATS server binary to ~/.local/bin/nats-server
|
|
5
|
+
# Idempotent: skips download if correct version already installed.
|
|
6
|
+
|
|
7
|
+
NATS_VERSION="v2.10.25"
|
|
8
|
+
INSTALL_DIR="$HOME/.local/bin"
|
|
9
|
+
INSTALL_PATH="$INSTALL_DIR/nats-server"
|
|
10
|
+
|
|
11
|
+
# Check if already installed at correct version
|
|
12
|
+
if [ -x "$INSTALL_PATH" ]; then
|
|
13
|
+
INSTALLED_VERSION=$("$INSTALL_PATH" --version 2>&1 | grep -oP 'v[\d.]+' || true)
|
|
14
|
+
if [ "$INSTALLED_VERSION" = "$NATS_VERSION" ]; then
|
|
15
|
+
echo "nats-server $NATS_VERSION already installed at $INSTALL_PATH"
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
echo "nats-server found but version mismatch (installed: $INSTALLED_VERSION, wanted: $NATS_VERSION). Upgrading..."
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
mkdir -p "$INSTALL_DIR"
|
|
22
|
+
|
|
23
|
+
ARCHIVE="nats-server-${NATS_VERSION}-linux-amd64.tar.gz"
|
|
24
|
+
URL="https://github.com/nats-io/nats-server/releases/download/${NATS_VERSION}/${ARCHIVE}"
|
|
25
|
+
|
|
26
|
+
echo "Downloading nats-server ${NATS_VERSION}..."
|
|
27
|
+
TMPDIR=$(mktemp -d)
|
|
28
|
+
trap 'rm -rf "$TMPDIR"' EXIT
|
|
29
|
+
|
|
30
|
+
curl -fsSL "$URL" -o "$TMPDIR/$ARCHIVE"
|
|
31
|
+
tar -xzf "$TMPDIR/$ARCHIVE" -C "$TMPDIR"
|
|
32
|
+
|
|
33
|
+
cp "$TMPDIR/nats-server-${NATS_VERSION}-linux-amd64/nats-server" "$INSTALL_PATH"
|
|
34
|
+
chmod +x "$INSTALL_PATH"
|
|
35
|
+
|
|
36
|
+
echo "Installed nats-server to $INSTALL_PATH"
|
|
37
|
+
"$INSTALL_PATH" --version
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as Sentry from '@sentry/nextjs';
|
|
2
|
+
|
|
3
|
+
Sentry.init({
|
|
4
|
+
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
5
|
+
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || 'development',
|
|
6
|
+
tracesSampleRate: 1.0,
|
|
7
|
+
sendDefaultPii: true,
|
|
8
|
+
integrations: [
|
|
9
|
+
Sentry.replayIntegration({
|
|
10
|
+
maskAllText: false,
|
|
11
|
+
blockAllMedia: false,
|
|
12
|
+
}),
|
|
13
|
+
],
|
|
14
|
+
replaysSessionSampleRate: 0.1,
|
|
15
|
+
replaysOnErrorSampleRate: 1.0,
|
|
16
|
+
allowUrls: [/https?:\/\/(.*\.)?khal\.ai/],
|
|
17
|
+
beforeSend(event) {
|
|
18
|
+
if (event.request?.url?.includes('/health')) return null;
|
|
19
|
+
return event;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as Sentry from '@sentry/nextjs';
|
|
2
|
+
|
|
3
|
+
Sentry.init({
|
|
4
|
+
dsn: process.env.SENTRY_DSN,
|
|
5
|
+
environment: process.env.SENTRY_ENVIRONMENT || 'development',
|
|
6
|
+
tracesSampleRate: 1.0,
|
|
7
|
+
sendDefaultPii: true,
|
|
8
|
+
beforeSend(event) {
|
|
9
|
+
if (event.request?.url?.includes('/health')) return null;
|
|
10
|
+
return event;
|
|
11
|
+
},
|
|
12
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as Sentry from '@sentry/nextjs';
|
|
2
|
+
|
|
3
|
+
Sentry.init({
|
|
4
|
+
dsn: process.env.SENTRY_DSN,
|
|
5
|
+
environment: process.env.SENTRY_ENVIRONMENT || 'development',
|
|
6
|
+
tracesSampleRate: 1.0,
|
|
7
|
+
sendDefaultPii: true,
|
|
8
|
+
beforeSend(event) {
|
|
9
|
+
if (event.request?.url?.includes('/health')) return null;
|
|
10
|
+
return event;
|
|
11
|
+
},
|
|
12
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { basename, extname } from 'node:path';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { withAuth } from '@workos-inc/authkit-nextjs';
|
|
6
|
+
import { escapeContentDisposition, getFilesRoot, resolveSafePath } from '@/lib/files/safe-path';
|
|
7
|
+
|
|
8
|
+
const MIME_TYPES: Record<string, string> = {
|
|
9
|
+
'.html': 'text/html',
|
|
10
|
+
'.css': 'text/css',
|
|
11
|
+
'.js': 'application/javascript',
|
|
12
|
+
'.json': 'application/json',
|
|
13
|
+
'.png': 'image/png',
|
|
14
|
+
'.jpg': 'image/jpeg',
|
|
15
|
+
'.jpeg': 'image/jpeg',
|
|
16
|
+
'.gif': 'image/gif',
|
|
17
|
+
'.svg': 'image/svg+xml',
|
|
18
|
+
'.webp': 'image/webp',
|
|
19
|
+
'.pdf': 'application/pdf',
|
|
20
|
+
'.txt': 'text/plain',
|
|
21
|
+
'.md': 'text/markdown',
|
|
22
|
+
'.csv': 'text/csv',
|
|
23
|
+
'.xml': 'application/xml',
|
|
24
|
+
'.zip': 'application/zip',
|
|
25
|
+
'.mp3': 'audio/mpeg',
|
|
26
|
+
'.mp4': 'video/mp4',
|
|
27
|
+
'.webm': 'video/webm',
|
|
28
|
+
'.woff': 'font/woff',
|
|
29
|
+
'.woff2': 'font/woff2',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function getMimeType(filePath: string): string {
|
|
33
|
+
const ext = extname(filePath).toLowerCase();
|
|
34
|
+
return MIME_TYPES[ext] || 'application/octet-stream';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function GET(request: Request) {
|
|
38
|
+
const { user } = await withAuth();
|
|
39
|
+
if (!user) {
|
|
40
|
+
return new Response('Unauthorized', { status: 401 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const url = new URL(request.url);
|
|
44
|
+
const filePath = url.searchParams.get('path');
|
|
45
|
+
|
|
46
|
+
if (!filePath) {
|
|
47
|
+
return Response.json({ error: 'Missing path parameter' }, { status: 400 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const root = getFilesRoot();
|
|
51
|
+
|
|
52
|
+
let resolved: string;
|
|
53
|
+
try {
|
|
54
|
+
resolved = resolveSafePath(root, filePath);
|
|
55
|
+
} catch {
|
|
56
|
+
return new Response('Forbidden', { status: 403 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let fileStat: Awaited<ReturnType<typeof stat>>;
|
|
60
|
+
try {
|
|
61
|
+
fileStat = await stat(resolved);
|
|
62
|
+
} catch {
|
|
63
|
+
return new Response('Not Found', { status: 404 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!fileStat.isFile()) {
|
|
67
|
+
return new Response('Not Found', { status: 404 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const name = basename(resolved);
|
|
71
|
+
const stream = createReadStream(resolved);
|
|
72
|
+
const webStream = Readable.toWeb(stream) as ReadableStream;
|
|
73
|
+
|
|
74
|
+
return new Response(webStream, {
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': getMimeType(resolved),
|
|
77
|
+
'Content-Disposition': escapeContentDisposition(name),
|
|
78
|
+
'Content-Length': String(fileStat.size),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readdirSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { PassThrough, Readable } from 'node:stream';
|
|
5
|
+
import { withAuth } from '@workos-inc/authkit-nextjs';
|
|
6
|
+
import archiver from 'archiver';
|
|
7
|
+
import { getFilesRoot, MAX_ZIP_ENTRIES, resolveSafePath } from '@/lib/files/safe-path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Recursively add directory contents to archive, validating each entry's
|
|
11
|
+
* real path stays within root (prevents symlink escape).
|
|
12
|
+
*/
|
|
13
|
+
function addDirectorySafe(archive: archiver.Archiver, root: string, dirPath: string, archivePrefix: string) {
|
|
14
|
+
const dirents = readdirSync(dirPath, { withFileTypes: true });
|
|
15
|
+
for (const dirent of dirents) {
|
|
16
|
+
const fullPath = join(dirPath, dirent.name);
|
|
17
|
+
// Check symlink escape per entry
|
|
18
|
+
const real = realpathSync(fullPath);
|
|
19
|
+
if (real !== root && !real.startsWith(`${root}/`)) {
|
|
20
|
+
continue; // Skip symlinks that escape root
|
|
21
|
+
}
|
|
22
|
+
const entryName = archivePrefix ? `${archivePrefix}/${dirent.name}` : dirent.name;
|
|
23
|
+
if (dirent.isDirectory()) {
|
|
24
|
+
addDirectorySafe(archive, root, fullPath, entryName);
|
|
25
|
+
} else {
|
|
26
|
+
archive.file(fullPath, { name: entryName });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function POST(request: Request) {
|
|
32
|
+
const { user } = await withAuth();
|
|
33
|
+
if (!user) {
|
|
34
|
+
return new Response('Unauthorized', { status: 401 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let body: { paths: string[] };
|
|
38
|
+
try {
|
|
39
|
+
body = await request.json();
|
|
40
|
+
} catch {
|
|
41
|
+
return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!Array.isArray(body.paths) || body.paths.length === 0) {
|
|
45
|
+
return Response.json({ error: 'Missing or empty paths array' }, { status: 400 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (body.paths.length > MAX_ZIP_ENTRIES) {
|
|
49
|
+
return Response.json({ error: `Too many entries (max ${MAX_ZIP_ENTRIES})` }, { status: 400 });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const root = getFilesRoot();
|
|
53
|
+
|
|
54
|
+
// Resolve and validate all paths up front
|
|
55
|
+
const resolvedPaths: { resolved: string; name: string; isDir: boolean }[] = [];
|
|
56
|
+
for (const p of body.paths) {
|
|
57
|
+
let resolved: string;
|
|
58
|
+
try {
|
|
59
|
+
resolved = resolveSafePath(root, p);
|
|
60
|
+
} catch {
|
|
61
|
+
return new Response('Forbidden', { status: 403 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let entryStat: Awaited<ReturnType<typeof stat>>;
|
|
65
|
+
try {
|
|
66
|
+
entryStat = await stat(resolved);
|
|
67
|
+
} catch {
|
|
68
|
+
return Response.json({ error: `Not found: ${p}` }, { status: 404 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
resolvedPaths.push({
|
|
72
|
+
resolved,
|
|
73
|
+
name: basename(resolved),
|
|
74
|
+
isDir: entryStat.isDirectory(),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const passthrough = new PassThrough();
|
|
79
|
+
const archive = archiver('zip', { zlib: { level: 5 } });
|
|
80
|
+
|
|
81
|
+
archive.pipe(passthrough);
|
|
82
|
+
|
|
83
|
+
for (const entry of resolvedPaths) {
|
|
84
|
+
if (entry.isDir) {
|
|
85
|
+
addDirectorySafe(archive, root, entry.resolved, entry.name);
|
|
86
|
+
} else {
|
|
87
|
+
archive.file(entry.resolved, { name: entry.name });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Finalize in background -- the stream will end when done
|
|
92
|
+
archive.finalize();
|
|
93
|
+
|
|
94
|
+
const webStream = Readable.toWeb(passthrough) as ReadableStream;
|
|
95
|
+
|
|
96
|
+
return new Response(webStream, {
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': 'application/zip',
|
|
99
|
+
'Content-Disposition': 'attachment; filename="download.zip"',
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { withAuth } from '@workos-inc/authkit-nextjs';
|
|
4
|
+
import { getFilesRoot, MAX_UPLOAD_SIZE, resolveSafePath, sanitizeFilename } from '@/lib/files/safe-path';
|
|
5
|
+
|
|
6
|
+
export async function POST(request: Request) {
|
|
7
|
+
const { user } = await withAuth();
|
|
8
|
+
if (!user) {
|
|
9
|
+
return new Response('Unauthorized', { status: 401 });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const formData = await request.formData();
|
|
14
|
+
const file = formData.get('file');
|
|
15
|
+
const targetPath = formData.get('path');
|
|
16
|
+
|
|
17
|
+
if (!(file instanceof File)) {
|
|
18
|
+
return Response.json({ ok: false, error: 'Missing file field' }, { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (file.size > MAX_UPLOAD_SIZE) {
|
|
22
|
+
return Response.json(
|
|
23
|
+
{ ok: false, error: `File too large (max ${MAX_UPLOAD_SIZE / 1024 / 1024} MB)` },
|
|
24
|
+
{ status: 413 }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const root = getFilesRoot();
|
|
29
|
+
const dirPath = typeof targetPath === 'string' && targetPath.length > 0 ? targetPath : '/';
|
|
30
|
+
|
|
31
|
+
let resolvedDir: string;
|
|
32
|
+
try {
|
|
33
|
+
resolvedDir = resolveSafePath(root, dirPath);
|
|
34
|
+
} catch {
|
|
35
|
+
return new Response('Forbidden', { status: 403 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await mkdir(resolvedDir, { recursive: true });
|
|
39
|
+
|
|
40
|
+
const safeName = sanitizeFilename(file.name);
|
|
41
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
42
|
+
const filePath = join(resolvedDir, safeName);
|
|
43
|
+
|
|
44
|
+
// Verify the final file path is also safe
|
|
45
|
+
try {
|
|
46
|
+
resolveSafePath(root, filePath.slice(root.length));
|
|
47
|
+
} catch {
|
|
48
|
+
return new Response('Forbidden', { status: 403 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await writeFile(filePath, bytes);
|
|
52
|
+
|
|
53
|
+
return Response.json({ ok: true, name: safeName, size: bytes.byteLength });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
56
|
+
return Response.json({ ok: false, error: message }, { status: 500 });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkOS webhook endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Receives signed webhook events from WorkOS and dispatches them to
|
|
5
|
+
* the appropriate handler for NATS broadcast.
|
|
6
|
+
*
|
|
7
|
+
* Signature verification uses the WorkOS SDK's `constructEvent()` method
|
|
8
|
+
* with the `WORKOS_WEBHOOK_SECRET` environment variable.
|
|
9
|
+
*
|
|
10
|
+
* This route is excluded from auth middleware via `unauthenticatedPaths`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getWorkOS } from '@workos-inc/authkit-nextjs';
|
|
14
|
+
import { handleMembershipDeleted, handleMembershipUpdated, handleUserUpdated } from '@/lib/auth/webhook-handler';
|
|
15
|
+
|
|
16
|
+
export async function POST(request: Request): Promise<Response> {
|
|
17
|
+
const secret = process.env.WORKOS_WEBHOOK_SECRET;
|
|
18
|
+
if (!secret) {
|
|
19
|
+
console.error('[webhook] WORKOS_WEBHOOK_SECRET is not configured');
|
|
20
|
+
return new Response('Webhook secret not configured', { status: 500 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sigHeader = request.headers.get('workos-signature');
|
|
24
|
+
if (!sigHeader) {
|
|
25
|
+
return new Response('Missing signature header', { status: 400 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let body: string;
|
|
29
|
+
try {
|
|
30
|
+
body = await request.text();
|
|
31
|
+
} catch {
|
|
32
|
+
return new Response('Invalid request body', { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let payload: Record<string, unknown>;
|
|
36
|
+
try {
|
|
37
|
+
payload = JSON.parse(body) as Record<string, unknown>;
|
|
38
|
+
} catch {
|
|
39
|
+
return new Response('Invalid JSON', { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Verify signature and construct typed event
|
|
43
|
+
const workos = getWorkOS();
|
|
44
|
+
const webhooks = workos.webhooks;
|
|
45
|
+
|
|
46
|
+
let event: Awaited<ReturnType<typeof webhooks.constructEvent>>;
|
|
47
|
+
try {
|
|
48
|
+
event = await webhooks.constructEvent({
|
|
49
|
+
payload,
|
|
50
|
+
sigHeader,
|
|
51
|
+
secret,
|
|
52
|
+
});
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const message = err instanceof Error ? err.message : 'Signature verification failed';
|
|
55
|
+
console.error('[webhook] signature verification failed:', message);
|
|
56
|
+
return new Response('Invalid signature', { status: 400 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log('[webhook] received event:', event.event);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
switch (event.event) {
|
|
63
|
+
case 'organization_membership.updated':
|
|
64
|
+
await handleMembershipUpdated(
|
|
65
|
+
event.data as {
|
|
66
|
+
userId: string;
|
|
67
|
+
organizationId: string;
|
|
68
|
+
id: string;
|
|
69
|
+
role: { slug: string };
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case 'organization_membership.deleted':
|
|
75
|
+
await handleMembershipDeleted(
|
|
76
|
+
event.data as {
|
|
77
|
+
userId: string;
|
|
78
|
+
organizationId: string;
|
|
79
|
+
id: string;
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 'user.updated':
|
|
85
|
+
await handleUserUpdated(event.data as { id: string; email: string });
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
default:
|
|
89
|
+
console.log('[webhook] unhandled event type:', event.event);
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error('[webhook] error processing event:', err);
|
|
93
|
+
// Return 200 to prevent WorkOS from retrying — log the error for investigation
|
|
94
|
+
return Response.json({ received: true, error: 'Processing failed' });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return Response.json({ received: true });
|
|
98
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { handleAuth } from '@workos-inc/authkit-nextjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AuthKit OAuth callback handler.
|
|
5
|
+
*
|
|
6
|
+
* WorkOS redirects here after a successful login. The SDK exchanges the
|
|
7
|
+
* authorization code for tokens, sets the session cookie, and redirects
|
|
8
|
+
* the user to /desktop.
|
|
9
|
+
*
|
|
10
|
+
* baseURL is required because Next.js runs behind Caddy and sees
|
|
11
|
+
* request.nextUrl as localhost:8888 instead of the public domain.
|
|
12
|
+
*/
|
|
13
|
+
const redirectUri = process.env.NEXT_PUBLIC_WORKOS_REDIRECT_URI ?? '';
|
|
14
|
+
const baseURL = redirectUri ? new URL(redirectUri).origin : undefined;
|
|
15
|
+
|
|
16
|
+
export const GET = handleAuth({ returnPathname: '/desktop', baseURL });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { signOut } from '@workos-inc/authkit-nextjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Server-side logout route (POST only to prevent CSRF).
|
|
5
|
+
*
|
|
6
|
+
* Clears the AuthKit session cookie and redirects to the root page,
|
|
7
|
+
* which the middleware will then redirect to the AuthKit hosted login.
|
|
8
|
+
*/
|
|
9
|
+
export async function POST() {
|
|
10
|
+
await signOut({ returnTo: '/' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function GET() {
|
|
14
|
+
return new Response('Method not allowed. Use POST.', { status: 405 });
|
|
15
|
+
}
|