clay-server 5.1.1-beta.1 → 5.2.0-beta.10
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/lib/account-auth-policy.js +21 -0
- package/lib/account-auth-socket.js +30 -0
- package/lib/account-auth-throttle.js +44 -0
- package/lib/account-auth-transport.js +48 -0
- package/lib/account-login-page.js +28 -0
- package/lib/account-passkey-challenges.js +31 -0
- package/lib/account-passkey-page.js +89 -0
- package/lib/account-passkey-policy.js +39 -0
- package/lib/account-password-page.js +69 -0
- package/lib/account-passwords.js +36 -0
- package/lib/account-remembered.js +77 -0
- package/lib/account-session-page.js +51 -0
- package/lib/account-session-store.js +82 -0
- package/lib/builtin-mates.js +19 -16
- package/lib/bundled-skills/clay-agent-browser/SKILL.md +6 -2
- package/lib/canonical-clay-record-authority.js +15 -0
- package/lib/canonical-clay-scheduling-authority.js +19 -0
- package/lib/claude-hook-installer.js +1 -239
- package/lib/claude-jsonl-watcher.js +1 -204
- package/lib/clay-control-presentation-binding.js +96 -0
- package/lib/clay-control-presentation.js +128 -0
- package/lib/clay-control-target-projection.js +26 -0
- package/lib/clay-mate-chat-receipts.js +26 -0
- package/lib/clay-mate-chat.js +88 -0
- package/lib/clay-mate-knowledge-input.js +26 -0
- package/lib/clay-mate-knowledge.js +95 -0
- package/lib/clay-mate-lifecycle.js +95 -0
- package/lib/clay-product-control-input.js +33 -0
- package/lib/clay-product-control-tools.js +51 -0
- package/lib/clay-product-control.js +247 -0
- package/lib/clay-project-lifecycle.js +154 -0
- package/lib/clay-project-record-input.js +44 -0
- package/lib/clay-project-records.js +84 -0
- package/lib/clay-scheduling-input.js +21 -0
- package/lib/clay-scheduling-receipts.js +26 -0
- package/lib/clay-scheduling.js +119 -0
- package/lib/clay-screen-controls.js +104 -0
- package/lib/clay-skill-install.js +1 -33
- package/lib/clay-workspace-controls.js +84 -0
- package/lib/clay-workspace-input.js +17 -0
- package/lib/clay-workspace-receipts.js +26 -0
- package/lib/cli-sessions.js +1 -300
- package/lib/codex-defaults.js +1 -28
- package/lib/context-usage.js +3 -2
- package/lib/contextual-screen-snapshot.js +38 -0
- package/lib/daemon-project-access.js +11 -0
- package/lib/daemon-project-lifecycle-callbacks.js +15 -0
- package/lib/daemon-project-lifecycle.js +262 -0
- package/lib/daemon-projects.js +1 -1
- package/lib/daemon.js +80 -276
- package/lib/default-ai-runtime.js +4 -8
- package/lib/dm-digest-cursor.js +120 -0
- package/lib/email-mcp-server.js +1 -1
- package/lib/issues-service.js +29 -15
- package/lib/kiro-defaults.js +1 -22
- package/lib/knowledge-db-import-validation.js +46 -0
- package/lib/knowledge-db-import.js +99 -0
- package/lib/knowledge-db-service.js +24 -33
- package/lib/knowledge-db-storage-nedb-maintenance.js +99 -0
- package/lib/knowledge-db-storage-nedb.js +66 -0
- package/lib/knowledge-db-storage.js +72 -0
- package/lib/knowledge-db-validation.js +0 -0
- package/lib/knowledge-document-index.js +2 -1
- package/lib/knowledge-file-store.js +7 -1
- package/lib/knowledge-product-operations.js +46 -0
- package/lib/mate-avatar-catalog.js +12 -0
- package/lib/mate-avatar-migration.js +41 -0
- package/lib/mate-context-service.js +138 -0
- package/lib/mate-context-tools.js +21 -0
- package/lib/mate-creation-mcp-server.js +2 -0
- package/lib/mate-knowledge-canonical-access.js +10 -0
- package/lib/mate-knowledge-mate-bound.js +53 -0
- package/lib/mate-knowledge-mcp-server.js +55 -8
- package/lib/mate-knowledge-service.js +16 -34
- package/lib/mate-knowledge-text-tools.js +106 -0
- package/lib/mate-lifecycle-service.js +94 -0
- package/lib/mate-message-delivery.js +43 -0
- package/lib/mate-ready-creation.js +9 -1
- package/lib/mate-schedule-input.js +29 -0
- package/lib/mate-schedule-lifecycle.js +151 -0
- package/lib/mate-scheduling.js +103 -0
- package/lib/mates.js +18 -6
- package/lib/mcp-agent-tools.js +117 -0
- package/lib/mcp-connection-store.js +119 -15
- package/lib/mcp-import-credentials.js +31 -0
- package/lib/mcp-local-transport.js +13 -0
- package/lib/mcp-oauth.js +8 -6
- package/lib/mcp-proxy.js +2 -32
- package/lib/mcp-query-bridge.js +30 -0
- package/lib/mcp-remote-connections.js +74 -20
- package/lib/mcp-source-adapters.js +1 -0
- package/lib/mcp-source-context.js +1 -0
- package/lib/mcp-source-discovery.js +1 -0
- package/lib/mcp-source-plugins.js +1 -0
- package/lib/mcp-source-restrictions.js +1 -0
- package/lib/mcp-tool-policy.js +9 -0
- package/lib/migrate-single-user.js +3 -23
- package/lib/notes-checklist.js +50 -0
- package/lib/notes.js +49 -3
- package/lib/os-user-diagnostics.js +2 -4
- package/lib/os-users.js +24 -70
- package/lib/pane-pending-interactions.js +20 -0
- package/lib/pane-session-activation.js +114 -0
- package/lib/pane-session-transport.js +26 -0
- package/lib/project-admin-overview.js +51 -0
- package/lib/project-ask-user.js +8 -4
- package/lib/project-connection.js +14 -10
- package/lib/project-control-git-view.js +12 -0
- package/lib/project-debate.js +1 -0
- package/lib/project-delegated-follow-up.js +2 -1
- package/lib/project-directory-claim.js +65 -0
- package/lib/project-file-operations.js +61 -0
- package/lib/project-filesystem.js +15 -85
- package/lib/project-full-access.js +23 -2
- package/lib/project-http.js +7 -4
- package/lib/project-knowledge.js +32 -18
- package/lib/project-lifecycle-operations.js +98 -0
- package/lib/project-lifecycle-process-witness.js +32 -0
- package/lib/project-lifecycle-runtime.js +50 -0
- package/lib/project-linux-access.js +22 -0
- package/lib/project-logs-canonical-clay.js +27 -0
- package/lib/project-logs-clay-query.js +16 -0
- package/lib/project-logs-schema.js +4 -2
- package/lib/project-logs-service.js +8 -7
- package/lib/project-logs.js +1 -0
- package/lib/project-loop.js +1 -0
- package/lib/project-mate-chat-control.js +55 -0
- package/lib/project-mate-creation-proposal.js +18 -3
- package/lib/project-mate-interaction.js +40 -62
- package/lib/project-mcp.js +54 -16
- package/lib/project-models.js +14 -31
- package/lib/project-notifications.js +76 -17
- package/lib/project-ownership.js +77 -0
- package/lib/project-pair-result-delivery.js +14 -3
- package/lib/project-pair-usage.js +12 -13
- package/lib/project-pending-message-queue.js +4 -1
- package/lib/project-product-control.js +89 -0
- package/lib/project-product-session-queue.js +46 -0
- package/lib/project-push-access.js +28 -0
- package/lib/project-scheduled-message-queue.js +30 -0
- package/lib/project-scheduled-messages.js +67 -68
- package/lib/project-scheduled-task-control.js +83 -0
- package/lib/project-scheduled-tasks.js +13 -2
- package/lib/project-scheduling-access.js +10 -0
- package/lib/project-server-settings.js +1 -0
- package/lib/project-session-actions.js +65 -0
- package/lib/project-session-delete.js +8 -1
- package/lib/project-session-document.js +27 -7
- package/lib/project-session-github.js +63 -19
- package/lib/project-session-history-actions.js +167 -0
- package/lib/project-session-linear.js +14 -6
- package/lib/project-session-notes.js +7 -3
- package/lib/project-session-pair.js +1 -1
- package/lib/project-session-runtime-actions.js +64 -0
- package/lib/project-session-spawn.js +2 -3
- package/lib/project-session-vendor.js +55 -0
- package/lib/project-sessions.js +103 -272
- package/lib/project-shared-browser.js +78 -17
- package/lib/project-studio-presentation.js +58 -0
- package/lib/project-user-knowledge.js +2 -1
- package/lib/project-user-message.js +67 -1
- package/lib/project-vendor-login.js +6 -36
- package/lib/project-workspace-operations.js +126 -0
- package/lib/project-workspace-query.js +19 -2
- package/lib/project.js +178 -45
- package/lib/public/app.js +46 -0
- package/lib/public/appearance-bootstrap.js +16 -0
- package/lib/public/auth/account-login.css +98 -0
- package/lib/public/auth/account-login.js +104 -0
- package/lib/public/css/clay-tool-activity.css +51 -0
- package/lib/public/css/clay-workspace.css +29 -0
- package/lib/public/css/command-palette.css +12 -33
- package/lib/public/css/contextual-clay.css +240 -0
- package/lib/public/css/home-debate-planning.css +2 -0
- package/lib/public/css/home-mate-creation.css +5 -0
- package/lib/public/css/input.css +6 -0
- package/lib/public/css/mate-avatar-gallery.css +12 -0
- package/lib/public/css/mate-conversation.css +10 -5
- package/lib/public/css/messages.css +6 -2
- package/lib/public/css/overlays.css +4 -2
- package/lib/public/css/project-admin-overview.css +13 -0
- package/lib/public/css/session-folders.css +11 -0
- package/lib/public/css/session-github.css +46 -1
- package/lib/public/css/shared-browser.css +35 -1
- package/lib/public/css/sticky-notes.css +87 -1
- package/lib/public/css/user-settings-sessions.css +42 -0
- package/lib/public/index.html +28 -10
- package/lib/public/mate-avatars/B01.png +0 -0
- package/lib/public/mate-avatars/B02.png +0 -0
- package/lib/public/mate-avatars/B03.png +0 -0
- package/lib/public/mate-avatars/B04.png +0 -0
- package/lib/public/mate-avatars/B05.png +0 -0
- package/lib/public/mate-avatars/B06.png +0 -0
- package/lib/public/mate-avatars/B07.png +0 -0
- package/lib/public/mate-avatars/B08.png +0 -0
- package/lib/public/mate-avatars/B09.png +0 -0
- package/lib/public/mate-avatars/B10.png +0 -0
- package/lib/public/mate-avatars/B11.png +0 -0
- package/lib/public/mate-avatars/B12.png +0 -0
- package/lib/public/mate-avatars/B13.png +0 -0
- package/lib/public/mate-avatars/B14.png +0 -0
- package/lib/public/mate-avatars/B15.png +0 -0
- package/lib/public/mate-avatars/B16.png +0 -0
- package/lib/public/mate-avatars/B17.png +0 -0
- package/lib/public/mate-avatars/B18.png +0 -0
- package/lib/public/mate-avatars/B19.png +0 -0
- package/lib/public/mate-avatars/B20.png +0 -0
- package/lib/public/mate-avatars/B21.png +0 -0
- package/lib/public/mate-avatars/B22.png +0 -0
- package/lib/public/mate-avatars/B23.png +0 -0
- package/lib/public/mate-avatars/B24.png +0 -0
- package/lib/public/mate-avatars/B25.png +0 -0
- package/lib/public/mate-avatars/B26.png +0 -0
- package/lib/public/mate-avatars/B27.png +0 -0
- package/lib/public/mate-avatars/B28.png +0 -0
- package/lib/public/mate-avatars/B29.png +0 -0
- package/lib/public/mate-avatars/B30.png +0 -0
- package/lib/public/mate-avatars/B31.png +0 -0
- package/lib/public/mate-avatars/B32.png +0 -0
- package/lib/public/mate-avatars/B33.png +0 -0
- package/lib/public/mate-avatars/B34.png +0 -0
- package/lib/public/mate-avatars/B35.png +0 -0
- package/lib/public/mate-avatars/B36.png +0 -0
- package/lib/public/mate-avatars/B37.png +0 -0
- package/lib/public/mate-avatars/B38.png +0 -0
- package/lib/public/mate-avatars/B39.png +0 -0
- package/lib/public/mate-avatars/B40.png +0 -0
- package/lib/public/mate-avatars/B41.png +0 -0
- package/lib/public/mate-avatars/B42.png +0 -0
- package/lib/public/mate-avatars/B43.png +0 -0
- package/lib/public/mate-avatars/B44.png +0 -0
- package/lib/public/mate-avatars/B45.png +0 -0
- package/lib/public/mate-avatars/B46.png +0 -0
- package/lib/public/mate-avatars/B47.png +0 -0
- package/lib/public/mate-avatars/B48.png +0 -0
- package/lib/public/mate-avatars/C01.png +0 -0
- package/lib/public/mate-avatars/C02.png +0 -0
- package/lib/public/mate-avatars/C03.png +0 -0
- package/lib/public/mate-avatars/C04.png +0 -0
- package/lib/public/mate-avatars/C05.png +0 -0
- package/lib/public/mate-avatars/C06.png +0 -0
- package/lib/public/mate-avatars/C07.png +0 -0
- package/lib/public/mate-avatars/C08.png +0 -0
- package/lib/public/mate-avatars/C09.png +0 -0
- package/lib/public/mate-avatars/C10.png +0 -0
- package/lib/public/mate-avatars/C11.png +0 -0
- package/lib/public/mate-avatars/C12.png +0 -0
- package/lib/public/mate-avatars/C13.png +0 -0
- package/lib/public/mate-avatars/C14.png +0 -0
- package/lib/public/mate-avatars/C15.png +0 -0
- package/lib/public/mate-avatars/C16.png +0 -0
- package/lib/public/mate-avatars/C17.png +0 -0
- package/lib/public/mate-avatars/C18.png +0 -0
- package/lib/public/mate-avatars/C19.png +0 -0
- package/lib/public/mate-avatars/C20.png +0 -0
- package/lib/public/mate-avatars/C21.png +0 -0
- package/lib/public/mate-avatars/C22.png +0 -0
- package/lib/public/mate-avatars/C23.png +0 -0
- package/lib/public/mate-avatars/C24.png +0 -0
- package/lib/public/mate-avatars/C25.png +0 -0
- package/lib/public/mate-avatars/C26.png +0 -0
- package/lib/public/mate-avatars/C27.png +0 -0
- package/lib/public/mate-avatars/C28.png +0 -0
- package/lib/public/mate-avatars/C29.png +0 -0
- package/lib/public/mate-avatars/C30.png +0 -0
- package/lib/public/mate-avatars/C31.png +0 -0
- package/lib/public/mate-avatars/C32.png +0 -0
- package/lib/public/mate-avatars/C33.png +0 -0
- package/lib/public/mate-avatars/C34.png +0 -0
- package/lib/public/mate-avatars/C35.png +0 -0
- package/lib/public/mate-avatars/C36.png +0 -0
- package/lib/public/mate-avatars/C37.png +0 -0
- package/lib/public/mate-avatars/C38.png +0 -0
- package/lib/public/mate-avatars/C39.png +0 -0
- package/lib/public/mate-avatars/C40.png +0 -0
- package/lib/public/mate-avatars/C41.png +0 -0
- package/lib/public/mate-avatars/C42.png +0 -0
- package/lib/public/mate-avatars/C43.png +0 -0
- package/lib/public/mate-avatars/C44.png +0 -0
- package/lib/public/mate-avatars/C45.png +0 -0
- package/lib/public/mate-avatars/C46.png +0 -0
- package/lib/public/mate-avatars/C47.png +0 -0
- package/lib/public/mate-avatars/C48.png +0 -0
- package/lib/public/mate-avatars/catalog.json +482 -0
- package/lib/public/mate-avatars/lead-clay.png +0 -0
- package/lib/public/modules/account-auth-settings.js +71 -0
- package/lib/public/modules/account-transport-settings.js +51 -0
- package/lib/public/modules/admin-settings-projects.js +6 -2
- package/lib/public/modules/app-connection.js +11 -1
- package/lib/public/modules/app-favicon.js +18 -9
- package/lib/public/modules/app-header.js +7 -4
- package/lib/public/modules/app-loop-ui.js +2 -0
- package/lib/public/modules/app-message-router.js +22 -1
- package/lib/public/modules/app-messages.js +119 -17
- package/lib/public/modules/app-misc.js +3 -1
- package/lib/public/modules/app-notifications.js +11 -5
- package/lib/public/modules/app-panels.js +61 -123
- package/lib/public/modules/app-projects.js +13 -0
- package/lib/public/modules/app-rate-limit.js +7 -27
- package/lib/public/modules/app-rendering.js +4 -3
- package/lib/public/modules/avatar.js +15 -41
- package/lib/public/modules/chat-render-runtime.js +20 -0
- package/lib/public/modules/clay-browser-links.js +47 -0
- package/lib/public/modules/clay-control-activity.js +214 -0
- package/lib/public/modules/clay-control-copy.js +36 -0
- package/lib/public/modules/clay-control-detail-targets.js +44 -0
- package/lib/public/modules/clay-control-object-targets.js +52 -0
- package/lib/public/modules/clay-control-target-visibility.js +12 -0
- package/lib/public/modules/clay-control-targets.js +44 -0
- package/lib/public/modules/clay-log-links.js +23 -0
- package/lib/public/modules/clay-navigation-render.js +52 -0
- package/lib/public/modules/clay-owner-navigation.js +33 -0
- package/lib/public/modules/clay-session-links.js +31 -0
- package/lib/public/modules/clay-studio-bridge.js +20 -0
- package/lib/public/modules/clay-studio-document.js +67 -0
- package/lib/public/modules/clay-studio-render.js +146 -0
- package/lib/public/modules/clay-tool-activity.js +140 -0
- package/lib/public/modules/clay-tool-presentation.js +342 -0
- package/lib/public/modules/command-palette.js +162 -40
- package/lib/public/modules/contextual-ask-session.js +26 -0
- package/lib/public/modules/contextual-clay-geometry.js +155 -0
- package/lib/public/modules/contextual-clay-session.js +130 -0
- package/lib/public/modules/contextual-clay-workspace.js +169 -0
- package/lib/public/modules/contextual-clay.js +250 -0
- package/lib/public/modules/contextual-composer.js +91 -0
- package/lib/public/modules/contextual-mate-context.js +117 -0
- package/lib/public/modules/contextual-mate-hint.js +40 -0
- package/lib/public/modules/contextual-mate-identity.js +23 -0
- package/lib/public/modules/contextual-mate-sessions.js +134 -0
- package/lib/public/modules/contextual-search.js +137 -0
- package/lib/public/modules/contextual-session-citations.js +58 -0
- package/lib/public/modules/debate.js +1 -0
- package/lib/public/modules/default-ai.js +11 -3
- package/lib/public/modules/elicitation-form.js +5 -0
- package/lib/public/modules/filebrowser.js +20 -1
- package/lib/public/modules/generation-stop.js +8 -5
- package/lib/public/modules/git-panel.js +3 -1
- package/lib/public/modules/history-replay-batch.js +13 -1
- package/lib/public/modules/home-debate-live.js +2 -0
- package/lib/public/modules/home-debate-planning.js +7 -7
- package/lib/public/modules/home-mate-avatar-editor.js +127 -208
- package/lib/public/modules/home-mate-chat.js +8 -6
- package/lib/public/modules/home-mate-creation.js +73 -1
- package/lib/public/modules/home-mate-settings.js +19 -12
- package/lib/public/modules/icon-strip-tooltip.js +2 -1
- package/lib/public/modules/input.js +88 -10
- package/lib/public/modules/issues-render.js +3 -2
- package/lib/public/modules/knowledge-db-workbench.js +4 -4
- package/lib/public/modules/markdown.js +6 -34
- package/lib/public/modules/mate-avatar-catalog.js +97 -0
- package/lib/public/modules/mate-knowledge-workbench.js +1 -1
- package/lib/public/modules/mate-management.js +40 -7
- package/lib/public/modules/mcp-connection-view.js +31 -13
- package/lib/public/modules/mcp-discovery-view.js +31 -0
- package/lib/public/modules/mcp-ui.js +23 -4
- package/lib/public/modules/mention.js +3 -0
- package/lib/public/modules/message-delivery.js +20 -2
- package/lib/public/modules/model-picker.js +2 -0
- package/lib/public/modules/pane-activation.js +214 -0
- package/lib/public/modules/pane-binding.js +82 -0
- package/lib/public/modules/pane-bridge.js +7 -4
- package/lib/public/modules/pane-browser-bridge.js +22 -0
- package/lib/public/modules/pane-file-bridge.js +4 -0
- package/lib/public/modules/pane-issue-bridge.js +4 -1
- package/lib/public/modules/passkey-browser.js +69 -0
- package/lib/public/modules/passkey-server-settings.js +57 -0
- package/lib/public/modules/pending-message-queue.js +1 -0
- package/lib/public/modules/permission-control.js +5 -4
- package/lib/public/modules/personal-settings-dialog.js +7 -1
- package/lib/public/modules/product-review.js +26 -0
- package/lib/public/modules/profile.js +1 -238
- package/lib/public/modules/project-access-recovery.js +17 -0
- package/lib/public/modules/project-admin-overview.js +21 -0
- package/lib/public/modules/project-chat-identity.js +13 -4
- package/lib/public/modules/project-logs-render.js +6 -2
- package/lib/public/modules/project-mate-navigation.js +144 -22
- package/lib/public/modules/rewind.js +5 -0
- package/lib/public/modules/scheduled-tasks.js +2 -0
- package/lib/public/modules/scheduler.js +2 -0
- package/lib/public/modules/search-clay-chat.js +5 -440
- package/lib/public/modules/server-settings-controls.js +10 -0
- package/lib/public/modules/server-settings.js +7 -2
- package/lib/public/modules/session-actions.js +5 -2
- package/lib/public/modules/session-context-menu.js +5 -0
- package/lib/public/modules/session-create-form.js +10 -10
- package/lib/public/modules/session-folder-layout.js +1 -1
- package/lib/public/modules/session-folder-view-menu.js +9 -0
- package/lib/public/modules/session-github.js +85 -14
- package/lib/public/modules/session-rich-drafts.js +21 -0
- package/lib/public/modules/session-switch-defer.js +67 -0
- package/lib/public/modules/session-work-primary.js +51 -0
- package/lib/public/modules/settings-defaults.js +27 -28
- package/lib/public/modules/shared-browser-settings.js +112 -0
- package/lib/public/modules/shared-browser-tabs.js +13 -3
- package/lib/public/modules/shared-browser.js +29 -6
- package/lib/public/modules/sidebar-mobile.js +4 -3
- package/lib/public/modules/sidebar-projects.js +1 -0
- package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
- package/lib/public/modules/sidebar-sessions.js +1 -0
- package/lib/public/modules/split-group-restore.js +68 -0
- package/lib/public/modules/split-pair-ui.js +1 -0
- package/lib/public/modules/split-pane-reconciler.js +15 -0
- package/lib/public/modules/split-pane-renderer.js +87 -6
- package/lib/public/modules/split-session-boundary.js +1 -1
- package/lib/public/modules/split-view.js +60 -68
- package/lib/public/modules/sticky-note-markdown.js +72 -6
- package/lib/public/modules/sticky-notes-actions.js +30 -0
- package/lib/public/modules/sticky-notes-browser.js +9 -2
- package/lib/public/modules/sticky-notes-card.js +23 -7
- package/lib/public/modules/sticky-notes-checklist.js +116 -0
- package/lib/public/modules/sticky-notes-editor.js +108 -44
- package/lib/public/modules/sticky-notes-shared.js +50 -2
- package/lib/public/modules/sticky-notes-state.js +66 -0
- package/lib/public/modules/sticky-notes.js +34 -51
- package/lib/public/modules/stt-controller.js +4 -1
- package/lib/public/modules/stt-recording.js +1 -1
- package/lib/public/modules/stt.js +7 -2
- package/lib/public/modules/terminal.js +9 -0
- package/lib/public/modules/theme.js +8 -1
- package/lib/public/modules/thinking-lifecycle.js +33 -3
- package/lib/public/modules/thinking-view.js +23 -1
- package/lib/public/modules/tools.js +60 -11
- package/lib/public/modules/tui-grab.js +1 -2
- package/lib/public/modules/user-settings-account.js +14 -1
- package/lib/public/modules/user-settings-appearance.js +10 -3
- package/lib/public/modules/user-settings-editors.js +1 -1
- package/lib/public/modules/user-settings-layout.js +11 -2
- package/lib/public/modules/user-settings-preferences.js +6 -2
- package/lib/public/modules/user-settings-profile.js +12 -2
- package/lib/public/modules/user-settings-sessions.js +120 -0
- package/lib/public/modules/user-settings.js +6 -2
- package/lib/public/modules/vendor-login-browser.js +5 -3
- package/lib/public/modules/vendor-login.js +2 -2
- package/lib/public/modules/vendor-priority.js +15 -1
- package/lib/public/modules/wireframe-workbench.js +5 -2
- package/lib/public/modules/you-activity.js +3 -1
- package/lib/public/modules/you.js +91 -6
- package/lib/public/muse-avatar.svg +4 -0
- package/lib/public/style.css +6 -0
- package/lib/public/sw.js +3 -1
- package/lib/safe-bash-commands.js +3 -45
- package/lib/scheduled-task-records.js +3 -1
- package/lib/scheduler.js +2 -0
- package/lib/sdk-bridge.js +342 -401
- package/lib/sdk-foreground-admission.js +16 -0
- package/lib/sdk-mcp-descriptors.js +2 -37
- package/lib/sdk-message-processor.js +52 -17
- package/lib/sdk-quota-snapshot.js +1 -1
- package/lib/sdk-response-recovery.js +1 -0
- package/lib/sdk-worker.js +2 -677
- package/lib/server-account-passkeys.js +186 -0
- package/lib/server-account-passwords.js +127 -0
- package/lib/server-account-security.js +53 -0
- package/lib/server-admin.js +63 -16
- package/lib/server-appearance-settings.js +13 -1
- package/lib/server-auth.js +107 -79
- package/lib/server-contextual-clay-settings.js +40 -0
- package/lib/server-contextual-mate-sessions.js +70 -0
- package/lib/server-dm.js +2 -1
- package/lib/server-global-ws.js +10 -20
- package/lib/server-home-chat-events.js +18 -4
- package/lib/server-home-chat.js +22 -20
- package/lib/server-home-clay-entry.js +23 -4
- package/lib/server-home-clay-session-links.js +29 -9
- package/lib/server-home-mate-creation.js +1 -0
- package/lib/server-home-mcp-consent.js +14 -0
- package/lib/server-mates.js +25 -62
- package/lib/server-owner-ui.js +118 -0
- package/lib/server-path-access.js +38 -0
- package/lib/server-project-removal.js +22 -0
- package/lib/server-settings.js +8 -2
- package/lib/server.js +165 -43
- package/lib/session-config-transition.js +45 -0
- package/lib/session-folders.js +7 -2
- package/lib/session-fork-history.js +21 -0
- package/lib/session-github.js +9 -2
- package/lib/session-hygiene.js +3 -35
- package/lib/session-permission-mode.js +4 -3
- package/lib/session-skill-scope.js +11 -0
- package/lib/session-visibility.js +5 -2
- package/lib/session-work-primary.js +50 -0
- package/lib/sessions.js +64 -99
- package/lib/shared-browser-cookie-sync.js +32 -0
- package/lib/shared-browser-cookies.js +79 -0
- package/lib/shared-browser-runtime.js +25 -9
- package/lib/shared-browser-storage-sync.js +43 -0
- package/lib/shared-browser-storage.js +101 -0
- package/lib/shared-browser-tools.js +2 -1
- package/lib/shared-browser-worker.js +46 -10
- package/lib/terminal-manager.js +65 -6
- package/lib/tool-llm.js +1 -1
- package/lib/tui-transcript-index.js +1 -132
- package/lib/user-knowledge-service.js +23 -1
- package/lib/users-auth.js +38 -3
- package/lib/users-contextual-clay-preferences.js +31 -0
- package/lib/users-create.js +97 -0
- package/lib/users-permissions.js +2 -8
- package/lib/users-server-pin.js +37 -0
- package/lib/users.js +18 -84
- package/lib/vendor-login-browser.js +1 -11
- package/lib/workspace-assignment-service.js +25 -0
- package/lib/workspace-query-access.js +4 -1
- package/lib/workspace-query-mcp-server.js +1 -1
- package/lib/workspace-query-service.js +13 -2
- package/lib/ws-request.js +1 -0
- package/lib/ws-schema.js +18 -2
- package/lib/yoke/acp-agent-profiles.js +7 -5
- package/lib/yoke/adapters/antigravity.js +3 -14
- package/lib/yoke/adapters/claude-query-contract.js +9 -0
- package/lib/yoke/adapters/claude.js +54 -50
- package/lib/yoke/adapters/codex-query-contract.js +24 -0
- package/lib/yoke/adapters/codex.js +53 -111
- package/lib/yoke/adapters/kiro.js +3 -1
- package/lib/yoke/adapters/muse-query-contract.js +7 -0
- package/lib/yoke/adapters/muse.js +122 -0
- package/lib/yoke/auth-events.js +17 -0
- package/lib/yoke/claude-hook-installer.js +271 -0
- package/lib/yoke/claude-jsonl-watcher.js +204 -0
- package/lib/yoke/claude-maintenance.js +55 -0
- package/lib/yoke/claude-permission-patterns.js +53 -0
- package/lib/yoke/claude-response-recovery.js +155 -0
- package/lib/yoke/claude-transcript-hygiene.js +34 -0
- package/lib/yoke/claude-worker-startup.js +28 -0
- package/lib/yoke/clay-skill-install.js +33 -0
- package/lib/yoke/cli-sessions.js +300 -0
- package/lib/yoke/codex-app-server.js +7 -12
- package/lib/yoke/codex-idle-recovery.js +81 -1
- package/lib/yoke/codex-recovery-admission.js +164 -0
- package/lib/yoke/configuration.js +13 -0
- package/lib/yoke/discovery.js +70 -0
- package/lib/yoke/errors.js +8 -0
- package/lib/yoke/host-environment.js +7 -0
- package/lib/yoke/index.js +9 -51
- package/lib/yoke/interface.js +116 -0
- package/lib/yoke/kiro-acp-server.js +5 -5
- package/lib/yoke/legacy-sessions.js +89 -0
- package/lib/yoke/login-protocol.js +25 -0
- package/lib/yoke/maintenance.js +12 -0
- package/lib/yoke/mcp-proxy.js +32 -0
- package/lib/yoke/mcp-source-adapters.js +91 -0
- package/lib/yoke/mcp-source-context.js +42 -0
- package/lib/yoke/mcp-source-discovery.js +169 -0
- package/lib/yoke/mcp-source-plugins.js +68 -0
- package/lib/yoke/mcp-source-restrictions.js +47 -0
- package/lib/yoke/mcp-tool-policy-normalize.js +28 -0
- package/lib/yoke/model-preferences.js +6 -0
- package/lib/yoke/muse-artifacts.js +36 -0
- package/lib/yoke/muse-controls.js +50 -0
- package/lib/yoke/muse-input.js +64 -0
- package/lib/yoke/muse-items.js +157 -0
- package/lib/yoke/muse-policy.js +52 -0
- package/lib/yoke/muse-query.js +428 -0
- package/lib/yoke/muse-runtime.js +211 -0
- package/lib/yoke/muse-services.js +79 -0
- package/lib/yoke/muse-skills.js +79 -0
- package/lib/yoke/muse-turns.js +71 -0
- package/lib/yoke/options/acp.js +2 -0
- package/lib/yoke/options/antigravity.js +2 -0
- package/lib/yoke/options/claude.js +21 -0
- package/lib/yoke/options/codex-defaults.js +29 -0
- package/lib/yoke/options/codex-settings.js +21 -0
- package/lib/yoke/options/codex.js +6 -0
- package/lib/yoke/options/kiro-defaults.js +22 -0
- package/lib/yoke/options/kiro.js +5 -0
- package/lib/yoke/options/muse.js +5 -0
- package/lib/yoke/provider-contract.js +5 -0
- package/lib/yoke/providers/antigravity-executable.js +8 -0
- package/lib/yoke/providers/claude-executable.js +40 -0
- package/lib/yoke/providers/claude-model-descriptions.js +14 -0
- package/lib/yoke/providers/claude-process.js +97 -0
- package/lib/yoke/providers/claude-provision.js +75 -0
- package/lib/yoke/providers/codex-errors.js +8 -0
- package/lib/yoke/providers/muse-executable.js +17 -0
- package/lib/yoke/provisioning.js +6 -0
- package/lib/yoke/query-options.js +10 -0
- package/lib/yoke/response-recovery.js +41 -0
- package/lib/yoke/sdk-mcp-descriptors.js +37 -0
- package/lib/yoke/sdk-server-factory.js +7 -0
- package/lib/yoke/session-history.js +8 -0
- package/lib/yoke/session-tool-transport.js +34 -13
- package/lib/yoke/settings.js +23 -0
- package/lib/yoke/source-locations.js +4 -0
- package/lib/yoke/title-query.js +18 -7
- package/lib/yoke/tui-transcript-index.js +132 -0
- package/lib/yoke/usage.js +48 -0
- package/lib/yoke/user-input.js +13 -0
- package/lib/yoke/vendor-registry.js +81 -1
- package/package.json +11 -4
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
var crypto = require('crypto');
|
|
2
|
+
var webauthn = require('@simplewebauthn/server');
|
|
3
|
+
var passwords = require('./account-passwords');
|
|
4
|
+
var policyStore = require('./account-passkey-policy');
|
|
5
|
+
|
|
6
|
+
function attachAccountPasskeys(ctx) {
|
|
7
|
+
var users = ctx.users;
|
|
8
|
+
var http = ctx.http;
|
|
9
|
+
var challenges = require('./account-passkey-challenges').attachChallenges(ctx);
|
|
10
|
+
function policy() { return policyStore.configured(users.loadUsers()); }
|
|
11
|
+
function eligible(user) { return user && user.passwordHash && !user.mustChangePin; }
|
|
12
|
+
function current(req, original) {
|
|
13
|
+
var user = ctx.getUser(req);
|
|
14
|
+
if (!eligible(user) || original && (user.id !== original.id || user.passwordHash !== original.passwordHash)) {
|
|
15
|
+
throw new Error('Your account credentials changed. Sign in again.');
|
|
16
|
+
}
|
|
17
|
+
return user;
|
|
18
|
+
}
|
|
19
|
+
async function reauthenticate(req, data, admin) {
|
|
20
|
+
var user = current(req);
|
|
21
|
+
if (admin && user.role !== 'admin') throw new Error('Administrator access required.');
|
|
22
|
+
http.throttle(req, user.username);
|
|
23
|
+
if (!await passwords.verify(data.currentPassword, user.passwordHash)) throw new Error('Current password is incorrect.');
|
|
24
|
+
user = current(req, user);
|
|
25
|
+
if (admin && user.role !== 'admin') throw new Error('Administrator access required.');
|
|
26
|
+
http.clearAttempts(req, user.username);
|
|
27
|
+
return user;
|
|
28
|
+
}
|
|
29
|
+
function samePolicy(expected) {
|
|
30
|
+
var now = policy();
|
|
31
|
+
if (!now || now.origin !== expected.origin || now.rpID !== expected.rpID) throw new Error('Passkey settings changed. Please retry.');
|
|
32
|
+
}
|
|
33
|
+
function label(value) {
|
|
34
|
+
if (typeof value !== 'string' || !value.trim() || Array.from(value.trim()).length > 80) throw new Error('Give this passkey a name of 1–80 characters.');
|
|
35
|
+
return value.trim();
|
|
36
|
+
}
|
|
37
|
+
function responseData(data) {
|
|
38
|
+
var response = data.response;
|
|
39
|
+
if (!response || typeof response.id !== 'string' || !response.response || typeof response.response.clientDataJSON !== 'string') throw new Error('Invalid passkey response.');
|
|
40
|
+
var client = JSON.parse(Buffer.from(response.response.clientDataJSON, 'base64url').toString('utf8'));
|
|
41
|
+
if (client.crossOrigin || client.topOrigin) throw new Error('Open Clay directly to use a passkey.');
|
|
42
|
+
return response;
|
|
43
|
+
}
|
|
44
|
+
function list(user) {
|
|
45
|
+
return (user.passkeys || []).map(function (key) { return { id: key.id, name: key.name, createdAt: key.createdAt, lastUsedAt: key.lastUsedAt || null }; });
|
|
46
|
+
}
|
|
47
|
+
async function registerOptions(req, res, data, config) {
|
|
48
|
+
var user = await reauthenticate(req, data);
|
|
49
|
+
var name = label(data.name);
|
|
50
|
+
if ((user.passkeys || []).length >= 10) throw new Error('You can register up to 10 passkeys.');
|
|
51
|
+
var handle = user.passkeyUserHandle || crypto.randomBytes(32).toString('base64url');
|
|
52
|
+
var options = await webauthn.generateRegistrationOptions({
|
|
53
|
+
rpName: 'Clay', rpID: config.rpID, userID: Buffer.from(handle, 'base64url'), userName: user.username,
|
|
54
|
+
attestationType: 'none', timeout: 60000, supportedAlgorithmIDs: [-7, -257],
|
|
55
|
+
authenticatorSelection: { residentKey: 'required', userVerification: 'required' },
|
|
56
|
+
excludeCredentials: (user.passkeys || []).map(function (key) { return { id: key.id, transports: key.transports }; }),
|
|
57
|
+
});
|
|
58
|
+
current(req, user); samePolicy(config);
|
|
59
|
+
var cookie = challenges.issue(req, config, { purpose: 'register', challenge: options.challenge, userId: user.id,
|
|
60
|
+
passwordHash: user.passwordHash, session: ctx.sessionToken(req), handle: handle, name: name });
|
|
61
|
+
http.reply(res, 200, { options: options }, cookie);
|
|
62
|
+
}
|
|
63
|
+
async function registerVerify(req, res, data, config) {
|
|
64
|
+
var proof = challenges.take(req, config, 'register');
|
|
65
|
+
var user = current(req, { id: proof.userId, passwordHash: proof.passwordHash });
|
|
66
|
+
if (ctx.sessionToken(req) !== proof.session) throw new Error('Use the same signed-in session to register this passkey.');
|
|
67
|
+
var response = responseData(data);
|
|
68
|
+
var result;
|
|
69
|
+
try {
|
|
70
|
+
result = await webauthn.verifyRegistrationResponse({ response: response, expectedChallenge: proof.challenge,
|
|
71
|
+
expectedOrigin: config.origin, expectedRPID: config.rpID, requireUserVerification: true, supportedAlgorithmIDs: [-7, -257] });
|
|
72
|
+
} catch (error) { throw new Error('Could not verify this passkey. Please try registering again.'); }
|
|
73
|
+
if (!result.verified) throw new Error('Could not verify this passkey.');
|
|
74
|
+
current(req, user); samePolicy(config);
|
|
75
|
+
if (proof.until <= Date.now()) throw new Error('Registration expired. Please try again.');
|
|
76
|
+
var state = users.loadUsers();
|
|
77
|
+
user = state.users.find(function (entry) { return entry.id === user.id; });
|
|
78
|
+
var info = result.registrationInfo; var key = info.credential;
|
|
79
|
+
if (key.id !== response.id || state.users.some(function (entry) { return (entry.passkeys || []).some(function (saved) { return saved.id === key.id; }); })) {
|
|
80
|
+
throw new Error('This passkey is already registered or has an invalid identifier.');
|
|
81
|
+
}
|
|
82
|
+
if ((user.passkeys || []).length >= 10 || user.passkeyUserHandle && user.passkeyUserHandle !== proof.handle) throw new Error('Passkeys changed. Please register again.');
|
|
83
|
+
user.passkeyUserHandle = proof.handle;
|
|
84
|
+
user.passkeys = (user.passkeys || []).concat([{ id: key.id, publicKey: Buffer.from(key.publicKey).toString('base64url'),
|
|
85
|
+
counter: key.counter, transports: key.transports, name: proof.name, createdAt: new Date().toISOString(),
|
|
86
|
+
deviceType: info.credentialDeviceType, backedUp: info.credentialBackedUp }]);
|
|
87
|
+
users.saveUsers(state);
|
|
88
|
+
http.reply(res, 200, { ok: true, passkeys: list(user) }, challenges.cookie('', config.origin));
|
|
89
|
+
}
|
|
90
|
+
async function loginOptions(req, res, config) {
|
|
91
|
+
http.throttle(req, 'passkey:' + ctx.clientAddress(req));
|
|
92
|
+
var options = await webauthn.generateAuthenticationOptions({ rpID: config.rpID, userVerification: 'required', timeout: 60000 });
|
|
93
|
+
samePolicy(config);
|
|
94
|
+
http.reply(res, 200, { options: options }, challenges.issue(req, config, { purpose: 'login', challenge: options.challenge }));
|
|
95
|
+
}
|
|
96
|
+
async function loginVerify(req, res, data, config) {
|
|
97
|
+
var proof = challenges.take(req, config, 'login');
|
|
98
|
+
var response = responseData(data);
|
|
99
|
+
var state = users.loadUsers();
|
|
100
|
+
var user = state.users.find(function (entry) { return (entry.passkeys || []).some(function (key) { return key.id === response.id; }); });
|
|
101
|
+
if (!eligible(user) || response.response.userHandle !== user.passkeyUserHandle) throw new Error('Could not sign in with this passkey.');
|
|
102
|
+
var key = user.passkeys.find(function (entry) { return entry.id === response.id; });
|
|
103
|
+
var result;
|
|
104
|
+
try {
|
|
105
|
+
result = await webauthn.verifyAuthenticationResponse({ response: response, expectedChallenge: proof.challenge,
|
|
106
|
+
expectedOrigin: config.origin, expectedRPID: config.rpID, requireUserVerification: true,
|
|
107
|
+
credential: { id: key.id, publicKey: new Uint8Array(Buffer.from(key.publicKey, 'base64url')), counter: key.counter, transports: key.transports } });
|
|
108
|
+
} catch (error) { throw new Error('Could not sign in with this passkey. Try again or use your password.'); }
|
|
109
|
+
if (!result.verified || result.authenticationInfo.credentialDeviceType !== key.deviceType) throw new Error('Could not sign in with this passkey.');
|
|
110
|
+
samePolicy(config);
|
|
111
|
+
state = users.loadUsers();
|
|
112
|
+
var latest = state.users.find(function (entry) { return entry.id === user.id; });
|
|
113
|
+
var latestKey = latest && (latest.passkeys || []).find(function (entry) { return entry.id === key.id; });
|
|
114
|
+
if (!eligible(latest) || latest.passwordHash !== user.passwordHash || !latestKey || latestKey.publicKey !== key.publicKey ||
|
|
115
|
+
latestKey.counter !== key.counter || proof.until <= Date.now()) throw new Error('Account credentials changed. Please try again.');
|
|
116
|
+
latestKey.counter = result.authenticationInfo.newCounter;
|
|
117
|
+
latestKey.backedUp = result.authenticationInfo.credentialBackedUp;
|
|
118
|
+
latestKey.lastUsedAt = new Date().toISOString();
|
|
119
|
+
users.saveUsers(state);
|
|
120
|
+
var session = ctx.createSession(latest.id, latest.passwordHash, key.id, config.origin.indexOf('https:') === 0, req);
|
|
121
|
+
http.clearAttempts(req, 'passkey:' + ctx.clientAddress(req));
|
|
122
|
+
http.reply(res, 200, { ok: true }, session.cookies.concat([challenges.cookie('', config.origin)]));
|
|
123
|
+
}
|
|
124
|
+
async function manage(req, res, data, config, remove) {
|
|
125
|
+
var user = await reauthenticate(req, data);
|
|
126
|
+
samePolicy(config);
|
|
127
|
+
var state = users.loadUsers();
|
|
128
|
+
user = state.users.find(function (entry) { return entry.id === user.id; });
|
|
129
|
+
var key = (user.passkeys || []).find(function (entry) { return entry.id === data.id; });
|
|
130
|
+
if (!key) throw new Error('Passkey not found.');
|
|
131
|
+
if (remove) user.passkeys = user.passkeys.filter(function (entry) { return entry.id !== key.id; });
|
|
132
|
+
else key.name = label(data.name);
|
|
133
|
+
users.saveUsers(state);
|
|
134
|
+
// Removing a passkey also invalidates its sessions through the live session gate.
|
|
135
|
+
var cookie = remove ? ctx.createSession(user.id, user.passwordHash, null, config.origin.indexOf('https:') === 0, req).cookies : undefined;
|
|
136
|
+
http.reply(res, 200, { ok: true, passkeys: list(user) }, cookie);
|
|
137
|
+
}
|
|
138
|
+
function handleRequest(req, res, url) {
|
|
139
|
+
if (url === '/auth/passkey-browser.js' && req.method === 'GET') {
|
|
140
|
+
res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-cache' });
|
|
141
|
+
res.end(require('fs').readFileSync(require('path').join(__dirname, 'public/modules/passkey-browser.js'), 'utf8')); return true;
|
|
142
|
+
}
|
|
143
|
+
if (url === '/auth/logout' && req.method === 'POST') challenges.clear(req);
|
|
144
|
+
if (url === '/account/passkeys' && req.method === 'GET') {
|
|
145
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'Content-Security-Policy': "frame-ancestors 'none'" });
|
|
146
|
+
res.end(eligible(ctx.getUser(req)) ? require('./account-passkey-page').render() : require('./account-password-page').render());
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (url === '/auth/passkeys/status' && req.method === 'GET') { http.reply(res, 200, { origin: policy() && policy().origin }); return true; }
|
|
150
|
+
if (url === '/api/passkey-policy') {
|
|
151
|
+
var admin = ctx.getUser(req);
|
|
152
|
+
if (!admin || admin.role !== 'admin') { http.reply(res, 403, { error: 'Administrator access required.' }); return true; }
|
|
153
|
+
if (req.method === 'GET') {
|
|
154
|
+
var state = users.loadUsers();
|
|
155
|
+
http.reply(res, 200, { available: state.accountAuthMethod === 'password', origin: policy() && policy().origin, locked: policyStore.count(state) > 0 }); return true;
|
|
156
|
+
}
|
|
157
|
+
if (req.method !== 'PUT') { http.reply(res, 405, { error: 'Method not allowed.' }); return true; }
|
|
158
|
+
http.read(req, res, async function (data) {
|
|
159
|
+
if (req.headers['sec-fetch-site'] === 'cross-site') throw new Error('Open Clay directly to change passkey settings.');
|
|
160
|
+
await reauthenticate(req, data, true);
|
|
161
|
+
var saved = policyStore.save(users, data.origin);
|
|
162
|
+
http.reply(res, 200, { ok: true, available: true, origin: saved.origin, locked: policyStore.count(users.loadUsers()) > 0 });
|
|
163
|
+
}); return true;
|
|
164
|
+
}
|
|
165
|
+
if (url.indexOf('/api/passkeys') !== 0 && url.indexOf('/auth/passkeys/') !== 0) return false;
|
|
166
|
+
var config = policy();
|
|
167
|
+
if (!config) { http.reply(res, 409, { error: 'Your administrator needs to configure passkeys first.' }); return true; }
|
|
168
|
+
if (url === '/api/passkeys' && req.method === 'GET') {
|
|
169
|
+
var user = ctx.getUser(req);
|
|
170
|
+
http.reply(res, eligible(user) ? 200 : 401, eligible(user) ? { origin: config.origin, passkeys: list(user) } : { error: 'Sign in to manage passkeys.' }); return true;
|
|
171
|
+
}
|
|
172
|
+
if (req.method !== 'POST') { http.reply(res, 405, { error: 'Method not allowed.' }); return true; }
|
|
173
|
+
if (req.headers.origin !== config.origin || req.headers['sec-fetch-site'] === 'cross-site') { http.reply(res, 403, { error: 'Use the configured Clay address to use passkeys.' }); return true; }
|
|
174
|
+
http.read(req, res, async function (data) {
|
|
175
|
+
if (url === '/api/passkeys/register/options') return registerOptions(req, res, data, config);
|
|
176
|
+
if (url === '/api/passkeys/register/verify') return registerVerify(req, res, data, config);
|
|
177
|
+
if (url === '/auth/passkeys/login/options') return loginOptions(req, res, config);
|
|
178
|
+
if (url === '/auth/passkeys/login/verify') return loginVerify(req, res, data, config);
|
|
179
|
+
if (url === '/api/passkeys/remove' || url === '/api/passkeys/rename') return manage(req, res, data, config, url.endsWith('/remove'));
|
|
180
|
+
http.reply(res, 404, { error: 'Passkey endpoint not found.' });
|
|
181
|
+
}, 32768);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return { handleRequest: handleRequest };
|
|
185
|
+
}
|
|
186
|
+
module.exports = { attachAccountPasskeys: attachAccountPasskeys };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
var crypto = require('crypto');
|
|
2
|
+
var passwords = require('./account-passwords');
|
|
3
|
+
var attachPolicy = require('./account-auth-policy').attachPolicy;
|
|
4
|
+
|
|
5
|
+
function attachAccountPasswords(ctx) {
|
|
6
|
+
var users = ctx.users;
|
|
7
|
+
var policy = attachPolicy(users);
|
|
8
|
+
var pending = new Map();
|
|
9
|
+
|
|
10
|
+
var cookieName = ctx.devMode ? 'clay_password_transition_dev' : 'clay_password_transition';
|
|
11
|
+
function reply(res, code, data, cookies) {
|
|
12
|
+
var headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' };
|
|
13
|
+
if (cookies) headers['Set-Cookie'] = cookies;
|
|
14
|
+
res.writeHead(code, headers); res.end(JSON.stringify(data));
|
|
15
|
+
}
|
|
16
|
+
function transitionCookie(token, req) {
|
|
17
|
+
return cookieName + '=' + token + '; Path=/; HttpOnly; SameSite=Strict; Max-Age=' + (token ? '600' : '0') + (ctx.tlsOptions || req && ctx.transport.info(req).secure ? '; Secure' : '');
|
|
18
|
+
}
|
|
19
|
+
function read(req, res, action, maxBytes) {
|
|
20
|
+
if ((req.headers['content-type'] || '').split(';')[0] !== 'application/json') { reply(res, 415, { error: 'JSON required.' }); return; }
|
|
21
|
+
var body = ''; var oversized = false;
|
|
22
|
+
req.on('data', function (chunk) { if (oversized) return; body += chunk; if (Buffer.byteLength(body) > (maxBytes || 4096)) { oversized = true; reply(res, 413, { error: 'Request too large.' }); } });
|
|
23
|
+
req.on('end', function () {
|
|
24
|
+
if (oversized) return;
|
|
25
|
+
var data; try { data = JSON.parse(body); } catch (error) { reply(res, 400, { error: 'Invalid request.' }); return; }
|
|
26
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) { reply(res, 400, { error: 'Invalid request.' }); return; }
|
|
27
|
+
Promise.resolve().then(function () { return action(data); }).catch(function (error) { reply(res, error.status || 400, { error: error.message || 'Could not complete sign-in.' }); });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function throttle(req, username) { ctx.throttle.consume(req, username); }
|
|
31
|
+
function clearAttempts(req, username) { ctx.throttle.success(req, username); }
|
|
32
|
+
function grant(user, res, req) {
|
|
33
|
+
var session = ctx.createSession(user.id, user.passwordHash, null, ctx.transport.info(req).secure, req);
|
|
34
|
+
reply(res, 200, { ok: true }, session.cookies.concat([transitionCookie('', req)]));
|
|
35
|
+
}
|
|
36
|
+
function startTransition(user, res, req) {
|
|
37
|
+
var now = Date.now();
|
|
38
|
+
pending.forEach(function (entry, key) { if (entry.until <= now) pending.delete(key); });
|
|
39
|
+
if (pending.size >= 1000) throw new Error('Sign-in is busy. Please retry shortly.');
|
|
40
|
+
var token = crypto.randomBytes(32).toString('hex');
|
|
41
|
+
pending.set(token, { userId: user.id, pinHash: user.pinHash, until: now + 600000, busy: false });
|
|
42
|
+
reply(res, 200, { ok: true, passwordRequired: true }, transitionCookie(token, req));
|
|
43
|
+
}
|
|
44
|
+
function handleRequest(req, res, url) {
|
|
45
|
+
if (req.method === 'POST' && url === '/auth/logout') pending.delete(ctx.parseCookies(req)[cookieName]);
|
|
46
|
+
if (url === '/api/account-auth-policy') {
|
|
47
|
+
var admin = ctx.getUser(req);
|
|
48
|
+
if (!admin || admin.role !== 'admin') { reply(res, 403, { error: 'Administrator access required.' }); return true; }
|
|
49
|
+
if (req.method === 'GET') { reply(res, 200, { method: policy.enabled() ? 'password' : 'pin', available: policy.available() }); return true; }
|
|
50
|
+
if (req.method !== 'PUT') { reply(res, 405, { error: 'Method not allowed.' }); return true; }
|
|
51
|
+
read(req, res, function (data) {
|
|
52
|
+
if (data.method !== 'password') throw new Error('Only the password transition is supported.');
|
|
53
|
+
policy.activate();
|
|
54
|
+
reply(res, 200, { ok: true, method: 'password' });
|
|
55
|
+
if (ctx.onPolicyChanged) ctx.onPolicyChanged();
|
|
56
|
+
});
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (!policy.enabled()) return false;
|
|
60
|
+
if (req.method === 'GET' && url === '/account/password') {
|
|
61
|
+
var signedIn = ctx.getUser(req);
|
|
62
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
63
|
+
res.end(signedIn ? require('./account-password-page').render(true) : require('./account-login-page').render({ password: true }));
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (req.method === 'POST' && url === '/auth/password-change') {
|
|
67
|
+
read(req, res, async function (data) {
|
|
68
|
+
var user = ctx.getUser(req);
|
|
69
|
+
if (!user) throw new Error('Sign in again to change your password.');
|
|
70
|
+
throttle(req, user.username);
|
|
71
|
+
if (!await passwords.verify(data.currentPassword, user.passwordHash)) throw new Error('Current password is incorrect.');
|
|
72
|
+
if (data.password !== data.confirmPassword) throw new Error('Passwords do not match.');
|
|
73
|
+
var hash = await passwords.hash(data.password);
|
|
74
|
+
var state = users.loadUsers();
|
|
75
|
+
var current = state.users.find(function (entry) { return entry.id === user.id; });
|
|
76
|
+
if (!current || current.passwordHash !== user.passwordHash || current.mustChangePin) throw new Error('Credentials changed. Sign in again.');
|
|
77
|
+
current.passwordHash = hash; users.saveUsers(state);
|
|
78
|
+
ctx.revokeSessions(user.id); clearAttempts(req, user.username); grant(current, res, req);
|
|
79
|
+
});
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (url === '/api/user/pin') {
|
|
83
|
+
reply(res, 409, { error: 'This server uses account passwords. Contact an administrator for credential recovery.' });
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (req.method === 'POST' && url === '/auth/login') {
|
|
87
|
+
read(req, res, async function (data) {
|
|
88
|
+
throttle(req, data.username);
|
|
89
|
+
var user = typeof data.username === 'string' ? users.findUserByUsername(data.username) : null;
|
|
90
|
+
if (data.pin !== undefined) {
|
|
91
|
+
if (!user || user.passwordHash && !user.mustChangePin || typeof data.pin !== 'string' || !/^\d{6}$/.test(data.pin) || !users.verifyPin(data.pin, user.pinHash, user)) throw new Error('Invalid username or account PIN.');
|
|
92
|
+
clearAttempts(req, user.username); startTransition(user, res, req); return;
|
|
93
|
+
}
|
|
94
|
+
var originalHash = user && user.passwordHash;
|
|
95
|
+
if (!await passwords.verify(data.password, originalHash)) throw new Error('Invalid username or password.');
|
|
96
|
+
user = users.findUserById(user.id);
|
|
97
|
+
if (!user || user.passwordHash !== originalHash || user.mustChangePin) throw new Error('Account credentials changed. Please sign in again.');
|
|
98
|
+
clearAttempts(req, user.username); grant(user, res, req);
|
|
99
|
+
});
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (req.method === 'POST' && url === '/auth/password-transition') {
|
|
103
|
+
read(req, res, async function (data) {
|
|
104
|
+
var token = ctx.parseCookies(req)[cookieName];
|
|
105
|
+
var proof = pending.get(token);
|
|
106
|
+
if (!proof || proof.until <= Date.now() || proof.busy) throw new Error('Verify your account PIN again.');
|
|
107
|
+
proof.busy = true;
|
|
108
|
+
try {
|
|
109
|
+
if (data.password !== data.confirmPassword) throw new Error('Passwords do not match.');
|
|
110
|
+
var hash = await passwords.hash(data.password);
|
|
111
|
+
var state = users.loadUsers();
|
|
112
|
+
var user = state.users.find(function (entry) { return entry.id === proof.userId; });
|
|
113
|
+
if (!user || user.pinHash !== proof.pinHash || user.passwordHash && !user.mustChangePin || proof.until <= Date.now()) throw new Error('Account credentials changed. Verify your PIN again.');
|
|
114
|
+
user.passwordHash = hash; user.pinHash = null; user.mustChangePin = false;
|
|
115
|
+
users.saveUsers(state);
|
|
116
|
+
pending.delete(token);
|
|
117
|
+
ctx.revokeSessions(user.id);
|
|
118
|
+
grant(user, res, req);
|
|
119
|
+
} finally { proof.busy = false; }
|
|
120
|
+
});
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return { handleRequest: handleRequest, policy: policy, http: { read: read, reply: reply, throttle: throttle, clearAttempts: clearAttempts } };
|
|
126
|
+
}
|
|
127
|
+
module.exports = { attachAccountPasswords: attachAccountPasswords };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
var passwords = require('./account-passwords');
|
|
2
|
+
function attachAccountSecurity(ctx) {
|
|
3
|
+
function sessionList(req, user) {
|
|
4
|
+
var current = ctx.sessions.get(ctx.sessionToken(req), false);
|
|
5
|
+
return ctx.sessions.list(user.id).filter(function (entry) { return !!ctx.validSessionUser(entry); }).map(function (entry) {
|
|
6
|
+
return { id: entry.id, current: !!current && entry.id === current.id, userAgent: entry.userAgent || '',
|
|
7
|
+
address: entry.address, createdAt: entry.migrated ? null : entry.createdAt,
|
|
8
|
+
lastSeenAt: entry.migrated ? entry.observedAt || null : entry.lastSeenAt,
|
|
9
|
+
unknownDevice: !!entry.migrated && !entry.observedAt,
|
|
10
|
+
expiresAt: entry.expiresAt, method: entry.passkeyId ? 'Passkey' : entry.credentialVersion ? 'Password' : 'Account sign-in', migrated: !!entry.migrated };
|
|
11
|
+
}).sort(function (a, b) { return Number(b.current) - Number(a.current) || (b.lastSeenAt || 0) - (a.lastSeenAt || 0); });
|
|
12
|
+
}
|
|
13
|
+
async function verify(req, data, original) {
|
|
14
|
+
ctx.http.throttle(req, original.username);
|
|
15
|
+
if (!original.passwordHash || !await passwords.verify(data.currentPassword, original.passwordHash)) throw new Error('Current password is incorrect.');
|
|
16
|
+
var current = ctx.getUser(req);
|
|
17
|
+
if (!current || current.id !== original.id || current.passwordHash !== original.passwordHash) throw new Error('Sign in again to continue.');
|
|
18
|
+
ctx.http.clearAttempts(req, current.username); return current;
|
|
19
|
+
}
|
|
20
|
+
function handleRequest(req, res, url) {
|
|
21
|
+
if (url !== '/api/account-sessions' && url !== '/api/account-transport' && url !== '/account/sessions') return false;
|
|
22
|
+
var user = ctx.getUser(req);
|
|
23
|
+
if (!user) { ctx.http.reply(res, 401, { error: 'Sign in again to manage your account.' }); return true; }
|
|
24
|
+
if (url === '/account/sessions' && req.method === 'GET') {
|
|
25
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'Content-Security-Policy': "frame-ancestors 'none'" });
|
|
26
|
+
res.end(require('./account-session-page').render()); return true;
|
|
27
|
+
}
|
|
28
|
+
if (url === '/api/account-transport') {
|
|
29
|
+
if (user.role !== 'admin') { ctx.http.reply(res, 403, { error: 'Administrator access required.' }); return true; }
|
|
30
|
+
if (req.method === 'GET') { ctx.http.reply(res, 200, ctx.transport.settings()); return true; }
|
|
31
|
+
if (req.method !== 'PUT') { ctx.http.reply(res, 405, { error: 'Method not allowed.' }); return true; }
|
|
32
|
+
ctx.http.read(req, res, async function (data) {
|
|
33
|
+
var admin = await verify(req, data, user);
|
|
34
|
+
if (admin.role !== 'admin') throw new Error('Administrator access required.');
|
|
35
|
+
var saved = ctx.transport.save(data); ctx.http.reply(res, 200, Object.assign({ ok: true }, saved));
|
|
36
|
+
}); return true;
|
|
37
|
+
}
|
|
38
|
+
if (req.method === 'GET') { ctx.http.reply(res, 200, { sessions: sessionList(req, user), maximumDays: 30, idleDays: 7 }); return true; }
|
|
39
|
+
if (req.method !== 'POST') { ctx.http.reply(res, 405, { error: 'Method not allowed.' }); return true; }
|
|
40
|
+
ctx.http.read(req, res, function (data) {
|
|
41
|
+
var active = ctx.getUser(req);
|
|
42
|
+
if (!active || active.id !== user.id) throw new Error('Sign in again to manage sessions.');
|
|
43
|
+
var current = ctx.sessions.get(ctx.sessionToken(req), false);
|
|
44
|
+
if (!current || typeof data.id !== 'string') throw new Error('Invalid session.');
|
|
45
|
+
if (data.id !== 'others' && !ctx.sessions.list(user.id).some(function (entry) { return entry.id === data.id; })) throw new Error('Session not found.');
|
|
46
|
+
ctx.sessions.revoke(user.id, data.id, current.id);
|
|
47
|
+
ctx.http.reply(res, 200, { ok: true, signedOut: data.id === current.id, sessions: sessionList(req, user) });
|
|
48
|
+
});
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return { handleRequest: handleRequest };
|
|
52
|
+
}
|
|
53
|
+
module.exports = { attachAccountSecurity: attachAccountSecurity };
|
package/lib/server-admin.js
CHANGED
|
@@ -18,6 +18,13 @@ function attachAdmin(ctx) {
|
|
|
18
18
|
var onGetProjectAccess = ctx.onGetProjectAccess;
|
|
19
19
|
var onProjectOwnerChanged = ctx.onProjectOwnerChanged;
|
|
20
20
|
|
|
21
|
+
function verifyCurrentOwner(req, route) {
|
|
22
|
+
if (!route || req.method === "GET" || !users.isMultiUser()) return;
|
|
23
|
+
var actor = getMultiUserFromReq(req);
|
|
24
|
+
var access = onGetProjectAccess && onGetProjectAccess(route[1]);
|
|
25
|
+
if (!actor || !access || access.ownerId !== actor.id || !users.findUserById(access.ownerId)) throw new Error("Project owner access required");
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
function handleRequest(req, res, fullUrl) {
|
|
22
29
|
var mateAccessRoute = /^\/api\/admin\/projects\/([^/]+)\/(visibility|owner|users|access)$/.exec(fullUrl);
|
|
23
30
|
if (mateAccessRoute) {
|
|
@@ -31,6 +38,25 @@ function attachAdmin(ctx) {
|
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
|
|
41
|
+
// Project content authority belongs to the valid owner, never the role.
|
|
42
|
+
if (mateAccessRoute && users.isMultiUser()) {
|
|
43
|
+
var actor = getMultiUserFromReq(req);
|
|
44
|
+
var governing = onGetProjectAccess && onGetProjectAccess(mateAccessRoute[1]);
|
|
45
|
+
var validOwner = governing && governing.ownerId && users.findUserById(governing.ownerId);
|
|
46
|
+
var owns = actor && validOwner && actor.id === validOwner.id;
|
|
47
|
+
if (req.method !== "GET" && !owns) {
|
|
48
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
49
|
+
res.end(JSON.stringify({ error: "Only the valid project owner can change access." }));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (req.method === "GET" && !owns && actor && actor.role === "admin") {
|
|
53
|
+
var overview = require("./project-admin-overview").metadataFor(actor, target && target.getStatus(), governing, users);
|
|
54
|
+
res.writeHead(overview ? 200 : 404, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
55
|
+
res.end(JSON.stringify(overview || { error: "Project not found" }));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
// --- Admin API endpoints (multi-user mode only) ---
|
|
35
61
|
|
|
36
62
|
// List all users (admin only)
|
|
@@ -83,6 +109,16 @@ function attachAdmin(ctx) {
|
|
|
83
109
|
// Look up the user before deletion to get linuxUser for deactivation
|
|
84
110
|
var targetUser = users.findUserById(targetUserId);
|
|
85
111
|
var targetLinuxUser = targetUser ? targetUser.linuxUser : null;
|
|
112
|
+
var owned = false;
|
|
113
|
+
projects.forEach(function (project, slug) {
|
|
114
|
+
var access = onGetProjectAccess && onGetProjectAccess(slug);
|
|
115
|
+
if (access && !access.isMate && access.ownerId === targetUserId) owned = true;
|
|
116
|
+
});
|
|
117
|
+
if (owned) {
|
|
118
|
+
res.writeHead(409, { "Content-Type": "application/json" });
|
|
119
|
+
res.end(JSON.stringify({ error: "The owner must transfer ordinary projects before account deletion." }));
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
86
122
|
var result = users.removeUser(targetUserId);
|
|
87
123
|
if (result.error) {
|
|
88
124
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -118,6 +154,7 @@ function attachAdmin(ctx) {
|
|
|
118
154
|
req.on("end", function () {
|
|
119
155
|
try {
|
|
120
156
|
var data = JSON.parse(body);
|
|
157
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
121
158
|
if (!data.username || typeof data.username !== "string" || data.username.trim().length < 1) {
|
|
122
159
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
123
160
|
res.end('{"error":"Username is required"}');
|
|
@@ -182,6 +219,11 @@ function attachAdmin(ctx) {
|
|
|
182
219
|
res.end('{"error":"User not found"}');
|
|
183
220
|
return true;
|
|
184
221
|
}
|
|
222
|
+
if (mu.id !== targetUserId) {
|
|
223
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
224
|
+
res.end(JSON.stringify({ error: "Use account-owner credential recovery. Administrators cannot impersonate another account." }));
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
185
227
|
var newPin = users.generatePin();
|
|
186
228
|
var pinResult = users.updateUserPin(targetUserId, newPin);
|
|
187
229
|
if (pinResult.error) {
|
|
@@ -222,15 +264,10 @@ function attachAdmin(ctx) {
|
|
|
222
264
|
req.on("data", function(chunk) { body += chunk; });
|
|
223
265
|
req.on("end", function() {
|
|
224
266
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
res.end(JSON.stringify({ error: result.error }));
|
|
230
|
-
} else {
|
|
231
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
232
|
-
res.end('{"ok":true}');
|
|
233
|
-
}
|
|
267
|
+
JSON.parse(body);
|
|
268
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
269
|
+
res.end(JSON.stringify({ error: "OS identity mapping requires server-side verified provisioning." }));
|
|
270
|
+
return;
|
|
234
271
|
} catch (e) {
|
|
235
272
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
236
273
|
res.end('{"error":"Invalid request body"}');
|
|
@@ -363,6 +400,7 @@ function attachAdmin(ctx) {
|
|
|
363
400
|
req.on("end", function () {
|
|
364
401
|
try {
|
|
365
402
|
var data = JSON.parse(body);
|
|
403
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
366
404
|
if (!data.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
|
367
405
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
368
406
|
res.end('{"error":"Valid email is required"}');
|
|
@@ -429,6 +467,7 @@ function attachAdmin(ctx) {
|
|
|
429
467
|
req.on("end", function () {
|
|
430
468
|
try {
|
|
431
469
|
var data = JSON.parse(body);
|
|
470
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
432
471
|
// Allow clearing SMTP config by sending empty fields
|
|
433
472
|
if (!data.host && !data.user && !data.pass && !data.from) {
|
|
434
473
|
smtp.saveSmtpConfig(null);
|
|
@@ -484,6 +523,7 @@ function attachAdmin(ctx) {
|
|
|
484
523
|
req.on("end", function () {
|
|
485
524
|
try {
|
|
486
525
|
var data = JSON.parse(body);
|
|
526
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
487
527
|
// Use provided config or fall back to saved
|
|
488
528
|
var existingCfg = smtp.getSmtpConfig();
|
|
489
529
|
var pass = data.pass;
|
|
@@ -548,6 +588,7 @@ function attachAdmin(ctx) {
|
|
|
548
588
|
req.on("end", function () {
|
|
549
589
|
try {
|
|
550
590
|
var data = JSON.parse(body);
|
|
591
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
551
592
|
if (data.visibility !== "public" && data.visibility !== "private") {
|
|
552
593
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
553
594
|
res.end('{"error":"Visibility must be public or private"}');
|
|
@@ -582,9 +623,10 @@ function attachAdmin(ctx) {
|
|
|
582
623
|
return true;
|
|
583
624
|
}
|
|
584
625
|
var mu = getMultiUserFromReq(req);
|
|
585
|
-
|
|
626
|
+
var ownerAccess = onGetProjectAccess && onGetProjectAccess(fullUrl.split("/")[4]);
|
|
627
|
+
if (!mu || !ownerAccess || mu.id !== ownerAccess.ownerId) {
|
|
586
628
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
587
|
-
res.end('{"error":"
|
|
629
|
+
res.end('{"error":"Project owner access required"}');
|
|
588
630
|
return true;
|
|
589
631
|
}
|
|
590
632
|
var projSlug = fullUrl.split("/")[4];
|
|
@@ -598,17 +640,21 @@ function attachAdmin(ctx) {
|
|
|
598
640
|
req.on("end", function () {
|
|
599
641
|
try {
|
|
600
642
|
var data = JSON.parse(body);
|
|
643
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
601
644
|
var targetCtx = projects.get(projSlug);
|
|
602
645
|
if (!targetCtx) {
|
|
603
646
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
604
647
|
res.end('{"error":"Project not found"}');
|
|
605
648
|
return;
|
|
606
649
|
}
|
|
607
|
-
var ownerId = data.userId
|
|
650
|
+
var ownerId = data.userId;
|
|
651
|
+
if (typeof ownerId !== "string" || !users.findUserById(ownerId)) throw new Error("A valid owner is required");
|
|
652
|
+
var currentAccess = onGetProjectAccess && onGetProjectAccess(projSlug);
|
|
653
|
+
if (!currentAccess || currentAccess.ownerId !== mu.id) throw new Error("Project owner access required");
|
|
654
|
+
if (!onProjectOwnerChanged) throw new Error("Ownership persistence is unavailable");
|
|
655
|
+
var saved = onProjectOwnerChanged(projSlug, ownerId);
|
|
656
|
+
if (saved && saved.error) throw new Error(saved.error);
|
|
608
657
|
targetCtx.setProjectOwner(ownerId);
|
|
609
|
-
if (onProjectOwnerChanged) {
|
|
610
|
-
onProjectOwnerChanged(projSlug, ownerId);
|
|
611
|
-
}
|
|
612
658
|
// Broadcast to project clients
|
|
613
659
|
var ownerName = null;
|
|
614
660
|
if (ownerId) {
|
|
@@ -653,7 +699,8 @@ function attachAdmin(ctx) {
|
|
|
653
699
|
req.on("end", function () {
|
|
654
700
|
try {
|
|
655
701
|
var data = JSON.parse(body);
|
|
656
|
-
|
|
702
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
703
|
+
if (!Array.isArray(data.allowedUsers) || data.allowedUsers.some(function (id) { return typeof id !== "string" || !users.findUserById(id); })) {
|
|
657
704
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
658
705
|
res.end('{"error":"allowedUsers must be an array"}');
|
|
659
706
|
return;
|
|
@@ -34,6 +34,18 @@ function attachAppearanceSettings(ctx) {
|
|
|
34
34
|
fs.renameSync(defaultPath + '.tmp', defaultPath);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
function getInitialAppearance(req) {
|
|
38
|
+
var user = identity(req);
|
|
39
|
+
if (!user) return null;
|
|
40
|
+
var saved = read(user);
|
|
41
|
+
return {
|
|
42
|
+
configured: saved.configured,
|
|
43
|
+
theme: saved.theme,
|
|
44
|
+
mode: saved.mode,
|
|
45
|
+
lightTheme: saved.lightTheme || null,
|
|
46
|
+
darkTheme: saved.darkTheme || null
|
|
47
|
+
};
|
|
48
|
+
}
|
|
37
49
|
function handleRequest(req, res, url) {
|
|
38
50
|
if (url !== '/api/user/appearance') return false;
|
|
39
51
|
var user = identity(req);
|
|
@@ -66,6 +78,6 @@ function attachAppearanceSettings(ctx) {
|
|
|
66
78
|
});
|
|
67
79
|
return true;
|
|
68
80
|
}
|
|
69
|
-
return { handleRequest: handleRequest };
|
|
81
|
+
return { handleRequest: handleRequest, getInitialAppearance: getInitialAppearance };
|
|
70
82
|
}
|
|
71
83
|
module.exports = { attachAppearanceSettings: attachAppearanceSettings };
|