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,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role hierarchy and normalization for Genie OS RBAC.
|
|
3
|
+
*
|
|
4
|
+
* Forward-compatible role slugs for Phase 1 (platform scope).
|
|
5
|
+
* WorkOS auto-prefixes org roles with `org-` in Phase 2,
|
|
6
|
+
* so platform roles use `platform-` prefix to avoid collisions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Canonical role hierarchy from least to most privileged. */
|
|
10
|
+
export const ROLE_HIERARCHY = ['member', 'platform-dev', 'platform-admin', 'platform-owner'] as const;
|
|
11
|
+
export type Role = (typeof ROLE_HIERARCHY)[number];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Map legacy / shorthand role slugs to canonical platform role slugs.
|
|
15
|
+
*
|
|
16
|
+
* Used during migration (admin → platform-admin) and for convenience
|
|
17
|
+
* (dev → platform-dev). WorkOS may return legacy slugs during the
|
|
18
|
+
* transition period.
|
|
19
|
+
*/
|
|
20
|
+
const ROLE_ALIASES: Record<string, Role> = {
|
|
21
|
+
// Legacy slugs (pre-Phase 1)
|
|
22
|
+
admin: 'platform-admin',
|
|
23
|
+
developer: 'platform-dev',
|
|
24
|
+
owner: 'platform-owner',
|
|
25
|
+
viewer: 'member',
|
|
26
|
+
user: 'member',
|
|
27
|
+
// Shorthand
|
|
28
|
+
dev: 'platform-dev',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Normalize a role string to a canonical platform role.
|
|
33
|
+
*
|
|
34
|
+
* Handles:
|
|
35
|
+
* - Canonical slugs (returned as-is)
|
|
36
|
+
* - Legacy slugs (mapped via ROLE_ALIASES)
|
|
37
|
+
* - Unknown strings (defaults to 'member')
|
|
38
|
+
*/
|
|
39
|
+
export function normalizeRole(role: string | undefined | null): Role {
|
|
40
|
+
if (!role) return 'member';
|
|
41
|
+
if (ROLE_HIERARCHY.includes(role as Role)) return role as Role;
|
|
42
|
+
return ROLE_ALIASES[role] ?? 'member';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if a role meets or exceeds the minimum required role level.
|
|
47
|
+
*/
|
|
48
|
+
export function hasMinRole(userRole: Role, minRole: Role): boolean {
|
|
49
|
+
return ROLE_HIERARCHY.indexOf(userRole) >= ROLE_HIERARCHY.indexOf(minRole);
|
|
50
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth abstraction layer types.
|
|
3
|
+
*
|
|
4
|
+
* These types decouple the application from any specific auth provider (WorkOS, BoxyHQ, etc.).
|
|
5
|
+
* All app code should depend on these interfaces, never on vendor types directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface Session {
|
|
9
|
+
userId: string;
|
|
10
|
+
orgId: string;
|
|
11
|
+
role: string;
|
|
12
|
+
permissions: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface User {
|
|
16
|
+
id: string;
|
|
17
|
+
email: string;
|
|
18
|
+
firstName?: string;
|
|
19
|
+
lastName?: string;
|
|
20
|
+
avatarUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Permissions {
|
|
24
|
+
apps: string[];
|
|
25
|
+
manageMembers: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AuthProvider {
|
|
29
|
+
verifySession(cookie: string): Promise<Session | null>;
|
|
30
|
+
getUser(userId: string): Promise<User>;
|
|
31
|
+
getOrgPermissions(orgId: string, userId: string): Promise<Permissions>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useAuth } from '@workos-inc/authkit-nextjs/components';
|
|
4
|
+
import { DEFAULT_ROLE_PERMISSIONS } from '@/components/apps/app-manifest';
|
|
5
|
+
import { normalizeRole } from '@/lib/auth/roles';
|
|
6
|
+
|
|
7
|
+
export interface KhalAuth {
|
|
8
|
+
userId: string;
|
|
9
|
+
orgId: string;
|
|
10
|
+
role: string;
|
|
11
|
+
permissions: string[];
|
|
12
|
+
loading: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Client-side hook that provides Khal auth state derived from the
|
|
17
|
+
* WorkOS AuthKit session. Returns null when not authenticated.
|
|
18
|
+
*
|
|
19
|
+
* Role is normalized to a canonical platform slug via normalizeRole().
|
|
20
|
+
* When no role is present, defaults to 'member' (least privilege).
|
|
21
|
+
*
|
|
22
|
+
* Permissions are derived from the normalized role using DEFAULT_ROLE_PERMISSIONS.
|
|
23
|
+
* WorkOS permissions (e.g., "widgets:users-table:manage") are org-level and don't
|
|
24
|
+
* map to app IDs — role-based defaults provide the correct app-level permissions.
|
|
25
|
+
*/
|
|
26
|
+
export function useKhalAuth(): KhalAuth | null {
|
|
27
|
+
const { user, role, loading } = useAuth();
|
|
28
|
+
|
|
29
|
+
if (loading) {
|
|
30
|
+
return { userId: '', orgId: '', role: '', permissions: [], loading: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!user) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const effectiveRole = normalizeRole(role);
|
|
38
|
+
const effectivePermissions = DEFAULT_ROLE_PERMISSIONS[effectiveRole] ?? [];
|
|
39
|
+
|
|
40
|
+
const instanceId = process.env.NEXT_PUBLIC_KHAL_INSTANCE_ID;
|
|
41
|
+
if (!instanceId) {
|
|
42
|
+
// biome-ignore lint/suspicious/noConsole: startup diagnostic for missing env var
|
|
43
|
+
console.error('[auth] NEXT_PUBLIC_KHAL_INSTANCE_ID is not set — NATS subjects will not match server');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
userId: user.id,
|
|
48
|
+
orgId: instanceId || 'default',
|
|
49
|
+
role: effectiveRole,
|
|
50
|
+
permissions: effectivePermissions,
|
|
51
|
+
loading: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkOS webhook event processing + NATS broadcast.
|
|
3
|
+
*
|
|
4
|
+
* Parses webhook payloads by event type and publishes role/membership
|
|
5
|
+
* changes to NATS subjects so connected WS clients can react in real-time.
|
|
6
|
+
*
|
|
7
|
+
* Session invalidation approach:
|
|
8
|
+
* - AuthKit manages sessions via encrypted cookies — no server-side session store.
|
|
9
|
+
* - On membership.deleted: broadcast via NATS; client-side listener forces logout.
|
|
10
|
+
* - On membership.updated (role change): broadcast via NATS; client-side listener
|
|
11
|
+
* refreshes role from server.
|
|
12
|
+
* - Sessions expire naturally per WorkOS session config; webhook provides faster feedback.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getNatsConnection } from '@/lib/nats';
|
|
16
|
+
|
|
17
|
+
/** NATS subjects for auth events. */
|
|
18
|
+
export const AUTH_SUBJECTS = {
|
|
19
|
+
roleChanged: 'os.auth.role-changed',
|
|
20
|
+
membershipRevoked: 'os.auth.membership-revoked',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export interface RoleChangedPayload {
|
|
24
|
+
userId: string;
|
|
25
|
+
newRole: string;
|
|
26
|
+
orgId: string;
|
|
27
|
+
membershipId: string;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MembershipRevokedPayload {
|
|
32
|
+
userId: string;
|
|
33
|
+
orgId: string;
|
|
34
|
+
membershipId: string;
|
|
35
|
+
timestamp: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Handle organization_membership.updated — extract new role and broadcast.
|
|
40
|
+
*/
|
|
41
|
+
export async function handleMembershipUpdated(data: {
|
|
42
|
+
userId: string;
|
|
43
|
+
organizationId: string;
|
|
44
|
+
id: string;
|
|
45
|
+
role: { slug: string };
|
|
46
|
+
}): Promise<void> {
|
|
47
|
+
const payload: RoleChangedPayload = {
|
|
48
|
+
userId: data.userId,
|
|
49
|
+
newRole: data.role.slug,
|
|
50
|
+
orgId: data.organizationId,
|
|
51
|
+
membershipId: data.id,
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
console.log('[webhook] membership updated — broadcasting role change:', payload);
|
|
56
|
+
|
|
57
|
+
const nc = await getNatsConnection();
|
|
58
|
+
nc.publish(AUTH_SUBJECTS.roleChanged, JSON.stringify(payload));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Handle organization_membership.deleted — broadcast membership revocation.
|
|
63
|
+
*/
|
|
64
|
+
export async function handleMembershipDeleted(data: {
|
|
65
|
+
userId: string;
|
|
66
|
+
organizationId: string;
|
|
67
|
+
id: string;
|
|
68
|
+
}): Promise<void> {
|
|
69
|
+
const payload: MembershipRevokedPayload = {
|
|
70
|
+
userId: data.userId,
|
|
71
|
+
orgId: data.organizationId,
|
|
72
|
+
membershipId: data.id,
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
console.log('[webhook] membership deleted — broadcasting revocation:', payload);
|
|
77
|
+
|
|
78
|
+
const nc = await getNatsConnection();
|
|
79
|
+
nc.publish(AUTH_SUBJECTS.membershipRevoked, JSON.stringify(payload));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle user.updated — log for now, extensible for future broadcasts.
|
|
84
|
+
*/
|
|
85
|
+
export async function handleUserUpdated(data: { id: string; email: string }): Promise<void> {
|
|
86
|
+
console.log('[webhook] user updated:', { userId: data.id, email: data.email });
|
|
87
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkOS implementation of the AuthProvider interface.
|
|
3
|
+
*
|
|
4
|
+
* Uses @workos-inc/authkit-nextjs SDK for session verification and
|
|
5
|
+
* @workos-inc/node (via getWorkOS()) for user/org management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getWorkOS, withAuth } from '@workos-inc/authkit-nextjs';
|
|
9
|
+
import { normalizeRole } from './roles';
|
|
10
|
+
import type { AuthProvider, Permissions, Session, User } from './types';
|
|
11
|
+
|
|
12
|
+
/** Default app permissions by role (forward-compatible Phase 1 slugs). */
|
|
13
|
+
const ROLE_DEFAULTS: Record<string, { apps: string[]; manageMembers: boolean }> = {
|
|
14
|
+
'platform-owner': { apps: ['*'], manageMembers: true },
|
|
15
|
+
'platform-admin': { apps: ['*'], manageMembers: true },
|
|
16
|
+
'platform-dev': { apps: ['terminal', 'files', 'settings', 'nats-viewer'], manageMembers: false },
|
|
17
|
+
member: { apps: ['files'], manageMembers: false },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class WorkOSAuthProvider implements AuthProvider {
|
|
21
|
+
async verifySession(_cookie: string): Promise<Session | null> {
|
|
22
|
+
try {
|
|
23
|
+
const session = await withAuth();
|
|
24
|
+
if (!session.user) return null;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
userId: session.user.id,
|
|
28
|
+
orgId: session.organizationId ?? '',
|
|
29
|
+
role: normalizeRole(session.role),
|
|
30
|
+
permissions: session.permissions ?? [],
|
|
31
|
+
};
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getUser(userId: string): Promise<User> {
|
|
38
|
+
const workos = getWorkOS();
|
|
39
|
+
const workosUser = await workos.userManagement.getUser(userId);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
id: workosUser.id,
|
|
43
|
+
email: workosUser.email,
|
|
44
|
+
firstName: workosUser.firstName ?? undefined,
|
|
45
|
+
lastName: workosUser.lastName ?? undefined,
|
|
46
|
+
avatarUrl: workosUser.profilePictureUrl ?? undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getOrgPermissions(orgId: string, _userId: string): Promise<Permissions> {
|
|
51
|
+
try {
|
|
52
|
+
const workos = getWorkOS();
|
|
53
|
+
const org = await workos.organizations.getOrganization(orgId);
|
|
54
|
+
|
|
55
|
+
if (org.metadata) {
|
|
56
|
+
const perms = JSON.parse(org.metadata.permissions ?? '{}') as Partial<Permissions>;
|
|
57
|
+
if (perms.apps) {
|
|
58
|
+
return { apps: perms.apps, manageMembers: perms.manageMembers ?? false };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Fall through to defaults
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return ROLE_DEFAULTS['platform-dev'];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const TASKBAR_HEIGHT = 44;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const KEY_PREFIX = 'khal:cmd:';
|
|
2
|
+
const TTL_MS = 10_000;
|
|
3
|
+
|
|
4
|
+
let _tabId: string;
|
|
5
|
+
function tabId() {
|
|
6
|
+
if (!_tabId) _tabId = Math.random().toString(36).slice(2);
|
|
7
|
+
return _tabId;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** djb2 hash — fast, deterministic, good distribution */
|
|
11
|
+
function djb2(str: string): string {
|
|
12
|
+
let hash = 5381;
|
|
13
|
+
for (let i = 0; i < str.length; i++) {
|
|
14
|
+
hash = (hash * 33) ^ str.charCodeAt(i);
|
|
15
|
+
}
|
|
16
|
+
return (hash >>> 0).toString(36);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Derive a stable cmdId from subject + payload. All tabs compute the same value. */
|
|
20
|
+
export function deriveCommandId(subject: string, payload: string): string {
|
|
21
|
+
const bucket = Math.floor(Date.now() / 5000); // 5s window
|
|
22
|
+
return djb2(subject + payload + bucket);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Try to claim a command via localStorage. Returns true if this tab should process it.
|
|
27
|
+
*
|
|
28
|
+
* Uses per-cmdId keys with write-then-verify (optimistic lock).
|
|
29
|
+
* Each tab writes its unique tabId, then reads back to confirm it won.
|
|
30
|
+
* This eliminates the TOCTOU race of the old JSON-blob approach.
|
|
31
|
+
*/
|
|
32
|
+
export function claimCommand(cmdId: string): boolean {
|
|
33
|
+
try {
|
|
34
|
+
const key = KEY_PREFIX + cmdId;
|
|
35
|
+
|
|
36
|
+
// Fast path: already claimed by another tab
|
|
37
|
+
if (localStorage.getItem(key) !== null) return false;
|
|
38
|
+
|
|
39
|
+
// Write our claim
|
|
40
|
+
const id = tabId();
|
|
41
|
+
localStorage.setItem(key, id);
|
|
42
|
+
|
|
43
|
+
// Verify we won the race (last writer wins, but read-back is consistent)
|
|
44
|
+
if (localStorage.getItem(key) !== id) return false;
|
|
45
|
+
|
|
46
|
+
// Schedule cleanup
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
try {
|
|
49
|
+
localStorage.removeItem(key);
|
|
50
|
+
} catch {}
|
|
51
|
+
}, TTL_MS);
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
// localStorage unavailable (SSR, private mode quota) — allow execution
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type Static, Type } from '@sinclair/typebox';
|
|
2
|
+
|
|
3
|
+
// ── Commands (inbound — agents publish, browser executes) ──
|
|
4
|
+
|
|
5
|
+
export const DesktopCmdOpen = Type.Object({
|
|
6
|
+
appId: Type.String(),
|
|
7
|
+
title: Type.Optional(Type.String()),
|
|
8
|
+
meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
9
|
+
width: Type.Optional(Type.Number()),
|
|
10
|
+
height: Type.Optional(Type.Number()),
|
|
11
|
+
});
|
|
12
|
+
export type DesktopCmdOpen = Static<typeof DesktopCmdOpen>;
|
|
13
|
+
|
|
14
|
+
export const DesktopCmdWindow = Type.Object({
|
|
15
|
+
windowId: Type.String(),
|
|
16
|
+
});
|
|
17
|
+
export type DesktopCmdWindow = Static<typeof DesktopCmdWindow>;
|
|
18
|
+
|
|
19
|
+
export const DesktopCmdNotify = Type.Object({
|
|
20
|
+
summary: Type.String(),
|
|
21
|
+
body: Type.Optional(Type.String()),
|
|
22
|
+
appName: Type.Optional(Type.String()),
|
|
23
|
+
urgency: Type.Optional(Type.Union([Type.Literal('low'), Type.Literal('normal'), Type.Literal('critical')])),
|
|
24
|
+
});
|
|
25
|
+
export type DesktopCmdNotify = Static<typeof DesktopCmdNotify>;
|
|
26
|
+
|
|
27
|
+
// ── Events (outbound — browser publishes, agents observe) ──
|
|
28
|
+
|
|
29
|
+
export const DesktopWindowEvent = Type.Object({
|
|
30
|
+
windowId: Type.String(),
|
|
31
|
+
appId: Type.String(),
|
|
32
|
+
title: Type.String(),
|
|
33
|
+
width: Type.Optional(Type.Number()),
|
|
34
|
+
height: Type.Optional(Type.Number()),
|
|
35
|
+
meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
36
|
+
});
|
|
37
|
+
export type DesktopWindowEvent = Static<typeof DesktopWindowEvent>;
|
|
38
|
+
|
|
39
|
+
export const DesktopWindowStateEntry = Type.Object({
|
|
40
|
+
id: Type.String(),
|
|
41
|
+
appId: Type.String(),
|
|
42
|
+
title: Type.String(),
|
|
43
|
+
minimized: Type.Boolean(),
|
|
44
|
+
maximized: Type.Boolean(),
|
|
45
|
+
focused: Type.Boolean(),
|
|
46
|
+
position: Type.Optional(Type.Object({ x: Type.Number(), y: Type.Number() })),
|
|
47
|
+
size: Type.Optional(Type.Object({ width: Type.Number(), height: Type.Number() })),
|
|
48
|
+
zIndex: Type.Optional(Type.Number()),
|
|
49
|
+
meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const DesktopEventState = Type.Object({
|
|
53
|
+
windows: Type.Array(DesktopWindowStateEntry),
|
|
54
|
+
});
|
|
55
|
+
export type DesktopEventState = Static<typeof DesktopEventState>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isomorphic filename validation — no Node.js dependencies.
|
|
3
|
+
* Safe to import from both client (React) and server (API routes / services).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const FORBIDDEN_CHARS = /[/\\\0]/;
|
|
7
|
+
const TRAVERSAL_SEGMENTS = /(?:^|\/)\.\.(?:\/|$)/;
|
|
8
|
+
const RESERVED_NAMES = /^\.+$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate a filename (not a path — just the basename).
|
|
12
|
+
* Returns an error message string if invalid, or `null` if valid.
|
|
13
|
+
*/
|
|
14
|
+
export function validateFilename(name: string): string | null {
|
|
15
|
+
const trimmed = name.trim();
|
|
16
|
+
if (trimmed.length === 0) return 'Name cannot be empty';
|
|
17
|
+
if (trimmed.length > 255) return 'Name is too long (max 255 characters)';
|
|
18
|
+
if (FORBIDDEN_CHARS.test(trimmed)) return 'Name contains forbidden characters (/ \\ or null)';
|
|
19
|
+
if (TRAVERSAL_SEGMENTS.test(trimmed)) return 'Name cannot contain path traversal (..)';
|
|
20
|
+
if (RESERVED_NAMES.test(trimmed)) return 'Name cannot be only dots';
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sanitize a filename by stripping dangerous characters.
|
|
26
|
+
* Throws if the result is empty after sanitization.
|
|
27
|
+
*/
|
|
28
|
+
export function sanitizeFilename(name: string): string {
|
|
29
|
+
// Strip null bytes, slashes, backslashes, and path traversal sequences
|
|
30
|
+
let clean = name
|
|
31
|
+
.replace(/\0/g, '')
|
|
32
|
+
.replace(/[/\\]/g, '')
|
|
33
|
+
.replace(/\.{2,}/g, '.')
|
|
34
|
+
.trim();
|
|
35
|
+
// Collapse leading dots to a single dot
|
|
36
|
+
clean = clean.replace(/^\.+/, '.');
|
|
37
|
+
if (clean.length === 0) {
|
|
38
|
+
throw new Error('Filename is empty after sanitization');
|
|
39
|
+
}
|
|
40
|
+
return clean;
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export { sanitizeFilename, validateFilename } from './filename-validation';
|
|
6
|
+
|
|
7
|
+
/** Max single-file upload size: 100 MB */
|
|
8
|
+
export const MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
/** Max entries in a single zip download */
|
|
11
|
+
export const MAX_ZIP_ENTRIES = 100;
|
|
12
|
+
|
|
13
|
+
export function getFilesRoot(): string {
|
|
14
|
+
return process.env.KHAL_FILES_ROOT || join(homedir(), 'khal-files');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a user-provided path safely within the root directory.
|
|
19
|
+
* Rejects path traversal attempts and symlink escapes.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveSafePath(root: string, userPath: string): string {
|
|
22
|
+
const resolved = resolve(root, userPath.replace(/^\/+/, ''));
|
|
23
|
+
if (resolved !== root && !resolved.startsWith(`${root}/`)) {
|
|
24
|
+
throw new Error('Path traversal detected');
|
|
25
|
+
}
|
|
26
|
+
// Symlink escape check: if the target exists, ensure its real path is within root
|
|
27
|
+
if (existsSync(resolved)) {
|
|
28
|
+
const real = realpathSync(resolved);
|
|
29
|
+
if (real !== root && !real.startsWith(`${root}/`)) {
|
|
30
|
+
throw new Error('Symlink escape detected');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Encode a filename for use in Content-Disposition headers (RFC 5987).
|
|
38
|
+
* Uses percent-encoding for non-ASCII and special characters.
|
|
39
|
+
*/
|
|
40
|
+
export function escapeContentDisposition(filename: string): string {
|
|
41
|
+
// ASCII fallback: replace non-ASCII with underscores
|
|
42
|
+
const asciiFallback = filename.replace(/[^\x20-\x7E]/g, '_').replace(/"/g, '\\"');
|
|
43
|
+
// RFC 5987 encoded value
|
|
44
|
+
const encoded = encodeURIComponent(filename).replace(
|
|
45
|
+
/['()]/g,
|
|
46
|
+
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
|
47
|
+
);
|
|
48
|
+
return `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encoded}`;
|
|
49
|
+
}
|