praxis-agent 0.1.0
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/LICENSE +21 -0
- package/README.md +534 -0
- package/dist/application/background-agent-manager.d.ts +66 -0
- package/dist/application/background-agent-manager.d.ts.map +1 -0
- package/dist/application/background-agent-manager.js +362 -0
- package/dist/application/background-agent-manager.js.map +1 -0
- package/dist/application/background-bash-manager.d.ts +54 -0
- package/dist/application/background-bash-manager.d.ts.map +1 -0
- package/dist/application/background-bash-manager.js +354 -0
- package/dist/application/background-bash-manager.js.map +1 -0
- package/dist/application/background-task-id.d.ts +5 -0
- package/dist/application/background-task-id.d.ts.map +1 -0
- package/dist/application/background-task-id.js +25 -0
- package/dist/application/background-task-id.js.map +1 -0
- package/dist/application/claude-project-purge.d.ts +50 -0
- package/dist/application/claude-project-purge.d.ts.map +1 -0
- package/dist/application/claude-project-purge.js +459 -0
- package/dist/application/claude-project-purge.js.map +1 -0
- package/dist/application/managed-worktree.d.ts +15 -0
- package/dist/application/managed-worktree.d.ts.map +1 -0
- package/dist/application/managed-worktree.js +127 -0
- package/dist/application/managed-worktree.js.map +1 -0
- package/dist/application/model-compactor.d.ts +8 -0
- package/dist/application/model-compactor.d.ts.map +1 -0
- package/dist/application/model-compactor.js +70 -0
- package/dist/application/model-compactor.js.map +1 -0
- package/dist/application/scheduled-prompt-manager.d.ts +69 -0
- package/dist/application/scheduled-prompt-manager.d.ts.map +1 -0
- package/dist/application/scheduled-prompt-manager.js +387 -0
- package/dist/application/scheduled-prompt-manager.js.map +1 -0
- package/dist/application/session-service.d.ts +142 -0
- package/dist/application/session-service.d.ts.map +1 -0
- package/dist/application/session-service.js +1690 -0
- package/dist/application/session-service.js.map +1 -0
- package/dist/application/session-worktree.d.ts +55 -0
- package/dist/application/session-worktree.d.ts.map +1 -0
- package/dist/application/session-worktree.js +311 -0
- package/dist/application/session-worktree.js.map +1 -0
- package/dist/application/subagent-service.d.ts +98 -0
- package/dist/application/subagent-service.d.ts.map +1 -0
- package/dist/application/subagent-service.js +1305 -0
- package/dist/application/subagent-service.js.map +1 -0
- package/dist/application/tool-use-summary.d.ts +8 -0
- package/dist/application/tool-use-summary.d.ts.map +1 -0
- package/dist/application/tool-use-summary.js +53 -0
- package/dist/application/tool-use-summary.js.map +1 -0
- package/dist/application/top-level-agent-manager.d.ts +58 -0
- package/dist/application/top-level-agent-manager.d.ts.map +1 -0
- package/dist/application/top-level-agent-manager.js +907 -0
- package/dist/application/top-level-agent-manager.js.map +1 -0
- package/dist/application/workflow-manager.d.ts +61 -0
- package/dist/application/workflow-manager.d.ts.map +1 -0
- package/dist/application/workflow-manager.js +582 -0
- package/dist/application/workflow-manager.js.map +1 -0
- package/dist/application/workflow-meta.d.ts +18 -0
- package/dist/application/workflow-meta.d.ts.map +1 -0
- package/dist/application/workflow-meta.js +146 -0
- package/dist/application/workflow-meta.js.map +1 -0
- package/dist/application/workflow-runtime.d.ts +29 -0
- package/dist/application/workflow-runtime.d.ts.map +1 -0
- package/dist/application/workflow-runtime.js +176 -0
- package/dist/application/workflow-runtime.js.map +1 -0
- package/dist/application/workflow-worktree.d.ts +9 -0
- package/dist/application/workflow-worktree.d.ts.map +1 -0
- package/dist/application/workflow-worktree.js +11 -0
- package/dist/application/workflow-worktree.js.map +1 -0
- package/dist/cli/agents-dashboard.d.ts +37 -0
- package/dist/cli/agents-dashboard.d.ts.map +1 -0
- package/dist/cli/agents-dashboard.js +408 -0
- package/dist/cli/agents-dashboard.js.map +1 -0
- package/dist/cli/controls.d.ts +15 -0
- package/dist/cli/controls.d.ts.map +1 -0
- package/dist/cli/controls.js +219 -0
- package/dist/cli/controls.js.map +1 -0
- package/dist/cli/debug.d.ts +13 -0
- package/dist/cli/debug.d.ts.map +1 -0
- package/dist/cli/debug.js +37 -0
- package/dist/cli/debug.js.map +1 -0
- package/dist/cli/interactive.d.ts +63 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +600 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/protocol.d.ts +251 -0
- package/dist/cli/protocol.d.ts.map +1 -0
- package/dist/cli/protocol.js +2146 -0
- package/dist/cli/protocol.js.map +1 -0
- package/dist/cli.d.ts +116 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3901 -0
- package/dist/cli.js.map +1 -0
- package/dist/compatibility/claude/compaction.d.ts +19 -0
- package/dist/compatibility/claude/compaction.d.ts.map +1 -0
- package/dist/compatibility/claude/compaction.js +82 -0
- package/dist/compatibility/claude/compaction.js.map +1 -0
- package/dist/compatibility/claude/context.d.ts +24 -0
- package/dist/compatibility/claude/context.d.ts.map +1 -0
- package/dist/compatibility/claude/context.js +92 -0
- package/dist/compatibility/claude/context.js.map +1 -0
- package/dist/compatibility/claude/dynamic-context.d.ts +20 -0
- package/dist/compatibility/claude/dynamic-context.d.ts.map +1 -0
- package/dist/compatibility/claude/dynamic-context.js +168 -0
- package/dist/compatibility/claude/dynamic-context.js.map +1 -0
- package/dist/compatibility/claude/file-history.d.ts +14 -0
- package/dist/compatibility/claude/file-history.d.ts.map +1 -0
- package/dist/compatibility/claude/file-history.js +340 -0
- package/dist/compatibility/claude/file-history.js.map +1 -0
- package/dist/compatibility/claude/file-resources.d.ts +28 -0
- package/dist/compatibility/claude/file-resources.d.ts.map +1 -0
- package/dist/compatibility/claude/file-resources.js +168 -0
- package/dist/compatibility/claude/file-resources.js.map +1 -0
- package/dist/compatibility/claude/fork.d.ts +9 -0
- package/dist/compatibility/claude/fork.d.ts.map +1 -0
- package/dist/compatibility/claude/fork.js +225 -0
- package/dist/compatibility/claude/fork.js.map +1 -0
- package/dist/compatibility/claude/history.d.ts +4 -0
- package/dist/compatibility/claude/history.d.ts.map +1 -0
- package/dist/compatibility/claude/history.js +86 -0
- package/dist/compatibility/claude/history.js.map +1 -0
- package/dist/compatibility/claude/ownership.d.ts +87 -0
- package/dist/compatibility/claude/ownership.d.ts.map +1 -0
- package/dist/compatibility/claude/ownership.js +100 -0
- package/dist/compatibility/claude/ownership.js.map +1 -0
- package/dist/compatibility/claude/paths.d.ts +17 -0
- package/dist/compatibility/claude/paths.d.ts.map +1 -0
- package/dist/compatibility/claude/paths.js +54 -0
- package/dist/compatibility/claude/paths.js.map +1 -0
- package/dist/compatibility/claude/pr-links.d.ts +25 -0
- package/dist/compatibility/claude/pr-links.d.ts.map +1 -0
- package/dist/compatibility/claude/pr-links.js +80 -0
- package/dist/compatibility/claude/pr-links.js.map +1 -0
- package/dist/compatibility/claude/projection.d.ts +11 -0
- package/dist/compatibility/claude/projection.d.ts.map +1 -0
- package/dist/compatibility/claude/projection.js +325 -0
- package/dist/compatibility/claude/projection.js.map +1 -0
- package/dist/compatibility/claude/schema.d.ts +18 -0
- package/dist/compatibility/claude/schema.d.ts.map +1 -0
- package/dist/compatibility/claude/schema.js +983 -0
- package/dist/compatibility/claude/schema.js.map +1 -0
- package/dist/compatibility/claude/shared-resources.d.ts +51 -0
- package/dist/compatibility/claude/shared-resources.d.ts.map +1 -0
- package/dist/compatibility/claude/shared-resources.js +382 -0
- package/dist/compatibility/claude/shared-resources.js.map +1 -0
- package/dist/compatibility/claude/sidechain.d.ts +50 -0
- package/dist/compatibility/claude/sidechain.d.ts.map +1 -0
- package/dist/compatibility/claude/sidechain.js +90 -0
- package/dist/compatibility/claude/sidechain.js.map +1 -0
- package/dist/compatibility/claude/tool-links.d.ts +13 -0
- package/dist/compatibility/claude/tool-links.d.ts.map +1 -0
- package/dist/compatibility/claude/tool-links.js +61 -0
- package/dist/compatibility/claude/tool-links.js.map +1 -0
- package/dist/compatibility/claude/translation.d.ts +71 -0
- package/dist/compatibility/claude/translation.d.ts.map +1 -0
- package/dist/compatibility/claude/translation.js +443 -0
- package/dist/compatibility/claude/translation.js.map +1 -0
- package/dist/compatibility/claude/workflow-replay.d.ts +10 -0
- package/dist/compatibility/claude/workflow-replay.d.ts.map +1 -0
- package/dist/compatibility/claude/workflow-replay.js +66 -0
- package/dist/compatibility/claude/workflow-replay.js.map +1 -0
- package/dist/compatibility/claude/workflow.d.ts +33 -0
- package/dist/compatibility/claude/workflow.d.ts.map +1 -0
- package/dist/compatibility/claude/workflow.js +76 -0
- package/dist/compatibility/claude/workflow.js.map +1 -0
- package/dist/core/compaction.d.ts +16 -0
- package/dist/core/compaction.d.ts.map +1 -0
- package/dist/core/compaction.js +2 -0
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/context-budget.d.ts +28 -0
- package/dist/core/context-budget.d.ts.map +1 -0
- package/dist/core/context-budget.js +108 -0
- package/dist/core/context-budget.js.map +1 -0
- package/dist/core/context.d.ts +16 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +13 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/runtime.d.ts +394 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +507 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/usage.d.ts +26 -0
- package/dist/core/usage.d.ts.map +1 -0
- package/dist/core/usage.js +133 -0
- package/dist/core/usage.js.map +1 -0
- package/dist/extensions/claude-extension-tools.d.ts +16 -0
- package/dist/extensions/claude-extension-tools.d.ts.map +1 -0
- package/dist/extensions/claude-extension-tools.js +78 -0
- package/dist/extensions/claude-extension-tools.js.map +1 -0
- package/dist/extensions/claude-extensions.d.ts +41 -0
- package/dist/extensions/claude-extensions.d.ts.map +1 -0
- package/dist/extensions/claude-extensions.js +270 -0
- package/dist/extensions/claude-extensions.js.map +1 -0
- package/dist/hooks/claude-hook-tools.d.ts +26 -0
- package/dist/hooks/claude-hook-tools.d.ts.map +1 -0
- package/dist/hooks/claude-hook-tools.js +129 -0
- package/dist/hooks/claude-hook-tools.js.map +1 -0
- package/dist/hooks/claude-hooks.d.ts +87 -0
- package/dist/hooks/claude-hooks.d.ts.map +1 -0
- package/dist/hooks/claude-hooks.js +411 -0
- package/dist/hooks/claude-hooks.js.map +1 -0
- package/dist/maintenance/doctor.d.ts +33 -0
- package/dist/maintenance/doctor.d.ts.map +1 -0
- package/dist/maintenance/doctor.js +391 -0
- package/dist/maintenance/doctor.js.map +1 -0
- package/dist/maintenance/self-update.d.ts +31 -0
- package/dist/maintenance/self-update.d.ts.map +1 -0
- package/dist/maintenance/self-update.js +81 -0
- package/dist/maintenance/self-update.js.map +1 -0
- package/dist/mcp/claude-mcp-management.d.ts +28 -0
- package/dist/mcp/claude-mcp-management.d.ts.map +1 -0
- package/dist/mcp/claude-mcp-management.js +169 -0
- package/dist/mcp/claude-mcp-management.js.map +1 -0
- package/dist/mcp/claude-mcp-oauth.d.ts +113 -0
- package/dist/mcp/claude-mcp-oauth.d.ts.map +1 -0
- package/dist/mcp/claude-mcp-oauth.js +1053 -0
- package/dist/mcp/claude-mcp-oauth.js.map +1 -0
- package/dist/mcp/claude-mcp-tools.d.ts +98 -0
- package/dist/mcp/claude-mcp-tools.d.ts.map +1 -0
- package/dist/mcp/claude-mcp-tools.js +1246 -0
- package/dist/mcp/claude-mcp-tools.js.map +1 -0
- package/dist/mcp/praxis-mcp-server.d.ts +35 -0
- package/dist/mcp/praxis-mcp-server.d.ts.map +1 -0
- package/dist/mcp/praxis-mcp-server.js +520 -0
- package/dist/mcp/praxis-mcp-server.js.map +1 -0
- package/dist/permissions/claude-auto-classifier.d.ts +20 -0
- package/dist/permissions/claude-auto-classifier.d.ts.map +1 -0
- package/dist/permissions/claude-auto-classifier.js +140 -0
- package/dist/permissions/claude-auto-classifier.js.map +1 -0
- package/dist/permissions/claude-permission-resolver.d.ts +30 -0
- package/dist/permissions/claude-permission-resolver.d.ts.map +1 -0
- package/dist/permissions/claude-permission-resolver.js +334 -0
- package/dist/permissions/claude-permission-resolver.js.map +1 -0
- package/dist/persistence/claude-job-store.d.ts +68 -0
- package/dist/persistence/claude-job-store.d.ts.map +1 -0
- package/dist/persistence/claude-job-store.js +311 -0
- package/dist/persistence/claude-job-store.js.map +1 -0
- package/dist/persistence/claude-scheduled-task-store.d.ts +29 -0
- package/dist/persistence/claude-scheduled-task-store.d.ts.map +1 -0
- package/dist/persistence/claude-scheduled-task-store.js +163 -0
- package/dist/persistence/claude-scheduled-task-store.js.map +1 -0
- package/dist/persistence/claude-sidechain-store.d.ts +18 -0
- package/dist/persistence/claude-sidechain-store.d.ts.map +1 -0
- package/dist/persistence/claude-sidechain-store.js +107 -0
- package/dist/persistence/claude-sidechain-store.js.map +1 -0
- package/dist/persistence/claude-task-store.d.ts +72 -0
- package/dist/persistence/claude-task-store.d.ts.map +1 -0
- package/dist/persistence/claude-task-store.js +440 -0
- package/dist/persistence/claude-task-store.js.map +1 -0
- package/dist/persistence/claude-transcript-store.d.ts +85 -0
- package/dist/persistence/claude-transcript-store.d.ts.map +1 -0
- package/dist/persistence/claude-transcript-store.js +385 -0
- package/dist/persistence/claude-transcript-store.js.map +1 -0
- package/dist/persistence/claude-workflow-store.d.ts +41 -0
- package/dist/persistence/claude-workflow-store.d.ts.map +1 -0
- package/dist/persistence/claude-workflow-store.js +263 -0
- package/dist/persistence/claude-workflow-store.js.map +1 -0
- package/dist/persistence/in-memory-sidechain-store.d.ts +12 -0
- package/dist/persistence/in-memory-sidechain-store.d.ts.map +1 -0
- package/dist/persistence/in-memory-sidechain-store.js +30 -0
- package/dist/persistence/in-memory-sidechain-store.js.map +1 -0
- package/dist/persistence/in-memory-transcript-store.d.ts +15 -0
- package/dist/persistence/in-memory-transcript-store.d.ts.map +1 -0
- package/dist/persistence/in-memory-transcript-store.js +95 -0
- package/dist/persistence/in-memory-transcript-store.js.map +1 -0
- package/dist/platform/atomic-write.d.ts +5 -0
- package/dist/platform/atomic-write.d.ts.map +1 -0
- package/dist/platform/atomic-write.js +25 -0
- package/dist/platform/atomic-write.js.map +1 -0
- package/dist/platform/bounded-process-runner.d.ts +28 -0
- package/dist/platform/bounded-process-runner.d.ts.map +1 -0
- package/dist/platform/bounded-process-runner.js +209 -0
- package/dist/platform/bounded-process-runner.js.map +1 -0
- package/dist/platform/claude-version.d.ts +5 -0
- package/dist/platform/claude-version.d.ts.map +1 -0
- package/dist/platform/claude-version.js +17 -0
- package/dist/platform/claude-version.js.map +1 -0
- package/dist/platform/command-shell.d.ts +3 -0
- package/dist/platform/command-shell.d.ts.map +1 -0
- package/dist/platform/command-shell.js +15 -0
- package/dist/platform/command-shell.js.map +1 -0
- package/dist/platform/exclusive-file-lease.d.ts +15 -0
- package/dist/platform/exclusive-file-lease.d.ts.map +1 -0
- package/dist/platform/exclusive-file-lease.js +265 -0
- package/dist/platform/exclusive-file-lease.js.map +1 -0
- package/dist/platform/sensitive-data.d.ts +8 -0
- package/dist/platform/sensitive-data.d.ts.map +1 -0
- package/dist/platform/sensitive-data.js +138 -0
- package/dist/platform/sensitive-data.js.map +1 -0
- package/dist/platform/tmux-worktree.d.ts +30 -0
- package/dist/platform/tmux-worktree.d.ts.map +1 -0
- package/dist/platform/tmux-worktree.js +138 -0
- package/dist/platform/tmux-worktree.js.map +1 -0
- package/dist/plugins/claude-plugin-eval-discovery.d.ts +20 -0
- package/dist/plugins/claude-plugin-eval-discovery.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-eval-discovery.js +128 -0
- package/dist/plugins/claude-plugin-eval-discovery.js.map +1 -0
- package/dist/plugins/claude-plugin-eval-graders.d.ts +49 -0
- package/dist/plugins/claude-plugin-eval-graders.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-eval-graders.js +137 -0
- package/dist/plugins/claude-plugin-eval-graders.js.map +1 -0
- package/dist/plugins/claude-plugin-eval-runner.d.ts +50 -0
- package/dist/plugins/claude-plugin-eval-runner.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-eval-runner.js +189 -0
- package/dist/plugins/claude-plugin-eval-runner.js.map +1 -0
- package/dist/plugins/claude-plugin-eval-schema.d.ts +80 -0
- package/dist/plugins/claude-plugin-eval-schema.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-eval-schema.js +436 -0
- package/dist/plugins/claude-plugin-eval-schema.js.map +1 -0
- package/dist/plugins/claude-plugin-eval.d.ts +66 -0
- package/dist/plugins/claude-plugin-eval.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-eval.js +438 -0
- package/dist/plugins/claude-plugin-eval.js.map +1 -0
- package/dist/plugins/claude-plugin-maintenance.d.ts +39 -0
- package/dist/plugins/claude-plugin-maintenance.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-maintenance.js +336 -0
- package/dist/plugins/claude-plugin-maintenance.js.map +1 -0
- package/dist/plugins/claude-plugin-marketplace.d.ts +97 -0
- package/dist/plugins/claude-plugin-marketplace.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-marketplace.js +1433 -0
- package/dist/plugins/claude-plugin-marketplace.js.map +1 -0
- package/dist/plugins/claude-plugin-mcpb.d.ts +71 -0
- package/dist/plugins/claude-plugin-mcpb.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-mcpb.js +885 -0
- package/dist/plugins/claude-plugin-mcpb.js.map +1 -0
- package/dist/plugins/claude-plugin-runtime.d.ts +120 -0
- package/dist/plugins/claude-plugin-runtime.d.ts.map +1 -0
- package/dist/plugins/claude-plugin-runtime.js +1263 -0
- package/dist/plugins/claude-plugin-runtime.js.map +1 -0
- package/dist/providers/anthropic-compatible.d.ts +35 -0
- package/dist/providers/anthropic-compatible.d.ts.map +1 -0
- package/dist/providers/anthropic-compatible.js +733 -0
- package/dist/providers/anthropic-compatible.js.map +1 -0
- package/dist/providers/environment.d.ts +14 -0
- package/dist/providers/environment.d.ts.map +1 -0
- package/dist/providers/environment.js +67 -0
- package/dist/providers/environment.js.map +1 -0
- package/dist/providers/fallback-provider.d.ts +17 -0
- package/dist/providers/fallback-provider.d.ts.map +1 -0
- package/dist/providers/fallback-provider.js +70 -0
- package/dist/providers/fallback-provider.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +30 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -0
- package/dist/providers/openai-compatible.js +409 -0
- package/dist/providers/openai-compatible.js.map +1 -0
- package/dist/tools/claude-interactive-tools.d.ts +58 -0
- package/dist/tools/claude-interactive-tools.d.ts.map +1 -0
- package/dist/tools/claude-interactive-tools.js +390 -0
- package/dist/tools/claude-interactive-tools.js.map +1 -0
- package/dist/tools/claude-lsp-formatters.d.ts +2 -0
- package/dist/tools/claude-lsp-formatters.d.ts.map +1 -0
- package/dist/tools/claude-lsp-formatters.js +291 -0
- package/dist/tools/claude-lsp-formatters.js.map +1 -0
- package/dist/tools/claude-lsp-tool.d.ts +22 -0
- package/dist/tools/claude-lsp-tool.d.ts.map +1 -0
- package/dist/tools/claude-lsp-tool.js +744 -0
- package/dist/tools/claude-lsp-tool.js.map +1 -0
- package/dist/tools/claude-scheduled-tools.d.ts +20 -0
- package/dist/tools/claude-scheduled-tools.d.ts.map +1 -0
- package/dist/tools/claude-scheduled-tools.js +344 -0
- package/dist/tools/claude-scheduled-tools.js.map +1 -0
- package/dist/tools/claude-task-tools.d.ts +30 -0
- package/dist/tools/claude-task-tools.d.ts.map +1 -0
- package/dist/tools/claude-task-tools.js +503 -0
- package/dist/tools/claude-task-tools.js.map +1 -0
- package/dist/tools/claude-user-message.d.ts +16 -0
- package/dist/tools/claude-user-message.d.ts.map +1 -0
- package/dist/tools/claude-user-message.js +98 -0
- package/dist/tools/claude-user-message.js.map +1 -0
- package/dist/tools/claude-workflow-tools.d.ts +27 -0
- package/dist/tools/claude-workflow-tools.d.ts.map +1 -0
- package/dist/tools/claude-workflow-tools.js +257 -0
- package/dist/tools/claude-workflow-tools.js.map +1 -0
- package/dist/tools/claude-worktree-tools.d.ts +17 -0
- package/dist/tools/claude-worktree-tools.d.ts.map +1 -0
- package/dist/tools/claude-worktree-tools.js +135 -0
- package/dist/tools/claude-worktree-tools.js.map +1 -0
- package/dist/tools/filtered-tool-registry.d.ts +16 -0
- package/dist/tools/filtered-tool-registry.d.ts.map +1 -0
- package/dist/tools/filtered-tool-registry.js +51 -0
- package/dist/tools/filtered-tool-registry.js.map +1 -0
- package/dist/tools/glob.d.ts +9 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +88 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/local-tools.d.ts +47 -0
- package/dist/tools/local-tools.d.ts.map +1 -0
- package/dist/tools/local-tools.js +1082 -0
- package/dist/tools/local-tools.js.map +1 -0
- package/dist/tools/notebook.d.ts +22 -0
- package/dist/tools/notebook.d.ts.map +1 -0
- package/dist/tools/notebook.js +154 -0
- package/dist/tools/notebook.js.map +1 -0
- package/dist/tools/web.d.ts +48 -0
- package/dist/tools/web.d.ts.map +1 -0
- package/dist/tools/web.js +726 -0
- package/dist/tools/web.js.map +1 -0
- package/package.json +145 -0
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { execFile, spawn } from 'node:child_process';
|
|
4
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
5
|
+
import { homedir, userInfo } from 'node:os';
|
|
6
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { auth, } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
9
|
+
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
10
|
+
import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const CREDENTIAL_SERVICE = 'Claude Code-credentials';
|
|
13
|
+
const MAX_CREDENTIAL_BYTES = 2 * 1024 * 1024;
|
|
14
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
15
|
+
const LOCK_WAIT_MS = 10_000;
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function stringValue(value) {
|
|
20
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
function validExpiresAt(value) {
|
|
23
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
function normalizeRecord(value) {
|
|
26
|
+
if (!isRecord(value))
|
|
27
|
+
return undefined;
|
|
28
|
+
const serverName = stringValue(value.serverName);
|
|
29
|
+
const serverUrl = stringValue(value.serverUrl);
|
|
30
|
+
if (!serverName || !serverUrl)
|
|
31
|
+
return undefined;
|
|
32
|
+
const discovery = isRecord(value.discoveryState)
|
|
33
|
+
? {
|
|
34
|
+
...(stringValue(value.discoveryState.authorizationServerUrl)
|
|
35
|
+
? {
|
|
36
|
+
authorizationServerUrl: value.discoveryState
|
|
37
|
+
.authorizationServerUrl,
|
|
38
|
+
}
|
|
39
|
+
: {}),
|
|
40
|
+
...(typeof value.discoveryState.oauthMetadataFound === 'boolean'
|
|
41
|
+
? { oauthMetadataFound: value.discoveryState.oauthMetadataFound }
|
|
42
|
+
: {}),
|
|
43
|
+
}
|
|
44
|
+
: undefined;
|
|
45
|
+
return {
|
|
46
|
+
serverName,
|
|
47
|
+
serverUrl,
|
|
48
|
+
accessToken: stringValue(value.accessToken) ?? '',
|
|
49
|
+
...(discovery === undefined ? {} : { discoveryState: discovery }),
|
|
50
|
+
...(stringValue(value.clientId)
|
|
51
|
+
? { clientId: value.clientId }
|
|
52
|
+
: {}),
|
|
53
|
+
...(stringValue(value.clientSecret)
|
|
54
|
+
? { clientSecret: value.clientSecret }
|
|
55
|
+
: {}),
|
|
56
|
+
...(stringValue(value.redirectUri)
|
|
57
|
+
? { redirectUri: value.redirectUri }
|
|
58
|
+
: {}),
|
|
59
|
+
...(stringValue(value.refreshToken)
|
|
60
|
+
? { refreshToken: value.refreshToken }
|
|
61
|
+
: {}),
|
|
62
|
+
...(validExpiresAt(value.expiresAt) === undefined
|
|
63
|
+
? {}
|
|
64
|
+
: { expiresAt: value.expiresAt }),
|
|
65
|
+
...(stringValue(value.scope) ? { scope: value.scope } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function stableServerConfig(server) {
|
|
69
|
+
return JSON.stringify({
|
|
70
|
+
type: server.type,
|
|
71
|
+
url: server.url,
|
|
72
|
+
headers: server.headers ?? {},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function mcpOAuthRecordKey(server) {
|
|
76
|
+
const digest = createHash('sha256')
|
|
77
|
+
.update(stableServerConfig(server))
|
|
78
|
+
.digest('hex')
|
|
79
|
+
.slice(0, 16);
|
|
80
|
+
return `${server.name}|${digest}`;
|
|
81
|
+
}
|
|
82
|
+
export function mcpOAuthCredentialService(configRoot) {
|
|
83
|
+
const canonical = resolve(configRoot);
|
|
84
|
+
const defaultRoot = resolve(join(homedir(), '.claude'));
|
|
85
|
+
if (canonical === defaultRoot)
|
|
86
|
+
return CREDENTIAL_SERVICE;
|
|
87
|
+
const digest = createHash('sha256')
|
|
88
|
+
.update(canonical)
|
|
89
|
+
.digest('hex')
|
|
90
|
+
.slice(0, 8);
|
|
91
|
+
return `${CREDENTIAL_SERVICE}-${digest}`;
|
|
92
|
+
}
|
|
93
|
+
function credentialPath(configRoot) {
|
|
94
|
+
return join(resolve(configRoot), '.credentials.json');
|
|
95
|
+
}
|
|
96
|
+
function parseEnvelope(source) {
|
|
97
|
+
let value;
|
|
98
|
+
try {
|
|
99
|
+
value = JSON.parse(source);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw new Error('Invalid MCP OAuth credential JSON', { cause: error });
|
|
103
|
+
}
|
|
104
|
+
if (!isRecord(value))
|
|
105
|
+
throw new Error('MCP OAuth credentials must be an object');
|
|
106
|
+
const envelope = { ...value };
|
|
107
|
+
if (envelope.mcpOAuth !== undefined && !isRecord(envelope.mcpOAuth)) {
|
|
108
|
+
throw new Error('MCP OAuth credentials mcpOAuth must be an object');
|
|
109
|
+
}
|
|
110
|
+
return envelope;
|
|
111
|
+
}
|
|
112
|
+
function sourceHash(source) {
|
|
113
|
+
return createHash('sha256').update(source).digest('hex');
|
|
114
|
+
}
|
|
115
|
+
function isSourceHash(value) {
|
|
116
|
+
return typeof value === 'string' && /^[a-f0-9]{64}$/u.test(value);
|
|
117
|
+
}
|
|
118
|
+
function parsePluginSecretTransactionJournal(value) {
|
|
119
|
+
if (!isRecord(value) ||
|
|
120
|
+
value.version !== 1 ||
|
|
121
|
+
typeof value.settingsPath !== 'string' ||
|
|
122
|
+
!isAbsolute(value.settingsPath) ||
|
|
123
|
+
!(value.settingsBeforeHash === null ||
|
|
124
|
+
isSourceHash(value.settingsBeforeHash)) ||
|
|
125
|
+
!isSourceHash(value.settingsAfterHash) ||
|
|
126
|
+
!isRecord(value.snapshot) ||
|
|
127
|
+
!isRecord(value.snapshot.envelope) ||
|
|
128
|
+
(value.snapshot.fileSource !== undefined &&
|
|
129
|
+
typeof value.snapshot.fileSource !== 'string') ||
|
|
130
|
+
(value.snapshot.keychainEnvelope !== undefined &&
|
|
131
|
+
!isRecord(value.snapshot.keychainEnvelope)) ||
|
|
132
|
+
!isRecord(value.nextEnvelope)) {
|
|
133
|
+
throw new Error('Invalid plugin configuration transaction journal');
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
function credentialAccount() {
|
|
138
|
+
try {
|
|
139
|
+
return userInfo().username;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return process.env.USER ?? 'user';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function readFileEnvelope(path) {
|
|
146
|
+
try {
|
|
147
|
+
const source = await readFile(path, 'utf8');
|
|
148
|
+
if (Buffer.byteLength(source) > MAX_CREDENTIAL_BYTES) {
|
|
149
|
+
throw new Error('MCP OAuth credential file exceeds 2 MiB');
|
|
150
|
+
}
|
|
151
|
+
return parseEnvelope(source);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error.code === 'ENOENT')
|
|
155
|
+
return {};
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function readFileEnvelopeSource(path) {
|
|
160
|
+
try {
|
|
161
|
+
const source = await readFile(path, 'utf8');
|
|
162
|
+
if (Buffer.byteLength(source) > MAX_CREDENTIAL_BYTES) {
|
|
163
|
+
throw new Error('MCP OAuth credential file exceeds 2 MiB');
|
|
164
|
+
}
|
|
165
|
+
return source;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error.code === 'ENOENT')
|
|
169
|
+
return undefined;
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function readKeychainEnvelope(service) {
|
|
174
|
+
try {
|
|
175
|
+
const result = await execFileAsync('security', ['find-generic-password', '-a', credentialAccount(), '-s', service, '-w'], { maxBuffer: MAX_CREDENTIAL_BYTES });
|
|
176
|
+
return parseEnvelope(result.stdout);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const code = error.code;
|
|
180
|
+
const detail = [
|
|
181
|
+
String(error),
|
|
182
|
+
String(error.stderr ?? ''),
|
|
183
|
+
].join('\n');
|
|
184
|
+
if (code === 'ENOENT' ||
|
|
185
|
+
/could not be found|SecKeychainSearchCopyNext/u.test(detail)) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function writeKeychainEnvelope(service, envelope) {
|
|
192
|
+
const source = JSON.stringify(envelope);
|
|
193
|
+
if (Buffer.byteLength(source) > MAX_CREDENTIAL_BYTES) {
|
|
194
|
+
throw new Error('MCP OAuth credentials exceed 2 MiB');
|
|
195
|
+
}
|
|
196
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
197
|
+
const child = spawn('security', [
|
|
198
|
+
'add-generic-password',
|
|
199
|
+
'-U',
|
|
200
|
+
'-a',
|
|
201
|
+
credentialAccount(),
|
|
202
|
+
'-s',
|
|
203
|
+
service,
|
|
204
|
+
'-w',
|
|
205
|
+
], { stdio: ['pipe', 'ignore', 'ignore'] });
|
|
206
|
+
let settled = false;
|
|
207
|
+
const rejectSafely = (code) => {
|
|
208
|
+
if (settled)
|
|
209
|
+
return;
|
|
210
|
+
settled = true;
|
|
211
|
+
const error = new Error('Unable to update macOS Keychain credentials');
|
|
212
|
+
if (code !== undefined)
|
|
213
|
+
error.code = code;
|
|
214
|
+
rejectPromise(error);
|
|
215
|
+
};
|
|
216
|
+
child.once('error', (error) => rejectSafely(error.code));
|
|
217
|
+
child.once('close', (code) => {
|
|
218
|
+
if (settled)
|
|
219
|
+
return;
|
|
220
|
+
settled = true;
|
|
221
|
+
if (code === 0)
|
|
222
|
+
resolvePromise();
|
|
223
|
+
else
|
|
224
|
+
rejectPromise(new Error('Unable to update macOS Keychain credentials'));
|
|
225
|
+
});
|
|
226
|
+
child.stdin.once('error', () => rejectSafely());
|
|
227
|
+
child.stdin.end(`${source}\n`);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async function deleteKeychainEnvelope(service) {
|
|
231
|
+
try {
|
|
232
|
+
await execFileAsync('security', ['delete-generic-password', '-a', credentialAccount(), '-s', service], { maxBuffer: MAX_CREDENTIAL_BYTES });
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
const code = error.code;
|
|
236
|
+
const detail = [
|
|
237
|
+
String(error),
|
|
238
|
+
String(error.stderr ?? ''),
|
|
239
|
+
].join('\n');
|
|
240
|
+
if (code === 'ENOENT' ||
|
|
241
|
+
/could not be found|SecKeychainSearchCopyNext/u.test(detail)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export class ClaudeMcpOAuthStore {
|
|
248
|
+
configRoot;
|
|
249
|
+
useKeychain;
|
|
250
|
+
service;
|
|
251
|
+
lease;
|
|
252
|
+
constructor(options) {
|
|
253
|
+
this.configRoot = resolve(options.configRoot);
|
|
254
|
+
this.useKeychain =
|
|
255
|
+
options.useKeychain ??
|
|
256
|
+
(process.platform === 'darwin' &&
|
|
257
|
+
process.env.PRAXIS_MCP_OAUTH_STORE !== 'file');
|
|
258
|
+
this.service = mcpOAuthCredentialService(this.configRoot);
|
|
259
|
+
this.lease = new ExclusiveFileLease(join(this.configRoot, '.mcp-oauth.lock'));
|
|
260
|
+
}
|
|
261
|
+
async read(server) {
|
|
262
|
+
return this.withLock(async () => {
|
|
263
|
+
const envelope = await this.readEnvelope();
|
|
264
|
+
return normalizeRecord(envelope.mcpOAuth?.[mcpOAuthRecordKey(server)]);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async mutate(server, update) {
|
|
268
|
+
return this.withLock(async () => {
|
|
269
|
+
const envelope = await this.readEnvelope();
|
|
270
|
+
const entries = isRecord(envelope.mcpOAuth)
|
|
271
|
+
? { ...envelope.mcpOAuth }
|
|
272
|
+
: {};
|
|
273
|
+
const key = mcpOAuthRecordKey(server);
|
|
274
|
+
const current = normalizeRecord(entries[key]);
|
|
275
|
+
const next = update(current);
|
|
276
|
+
if (next === undefined)
|
|
277
|
+
delete entries[key];
|
|
278
|
+
else
|
|
279
|
+
entries[key] = next;
|
|
280
|
+
if (Object.keys(entries).length === 0)
|
|
281
|
+
delete envelope.mcpOAuth;
|
|
282
|
+
else
|
|
283
|
+
envelope.mcpOAuth = entries;
|
|
284
|
+
await this.writeEnvelope(envelope);
|
|
285
|
+
return next;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async clear(server) {
|
|
289
|
+
await this.mutate(server, () => undefined);
|
|
290
|
+
}
|
|
291
|
+
async readClientSecret(server) {
|
|
292
|
+
return this.withLock(async () => {
|
|
293
|
+
const envelope = await this.readEnvelope();
|
|
294
|
+
return stringValue(envelope.mcpOAuthClientConfig?.[mcpOAuthRecordKey(server)]
|
|
295
|
+
?.clientSecret);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async saveClientSecret(server, clientSecret) {
|
|
299
|
+
await this.withLock(async () => {
|
|
300
|
+
const envelope = await this.readEnvelope();
|
|
301
|
+
const existing = isRecord(envelope.mcpOAuthClientConfig)
|
|
302
|
+
? { ...envelope.mcpOAuthClientConfig }
|
|
303
|
+
: {};
|
|
304
|
+
const key = mcpOAuthRecordKey(server);
|
|
305
|
+
const current = isRecord(existing[key]) ? existing[key] : {};
|
|
306
|
+
existing[key] = { ...current, clientSecret };
|
|
307
|
+
envelope.mcpOAuthClientConfig = existing;
|
|
308
|
+
await this.writeEnvelope(envelope);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async readPluginSecrets(pluginId) {
|
|
312
|
+
return this.withLock(async () => {
|
|
313
|
+
const envelope = await this.readEnvelope();
|
|
314
|
+
const stored = envelope.pluginSecrets?.[pluginId];
|
|
315
|
+
if (!isRecord(stored))
|
|
316
|
+
return {};
|
|
317
|
+
return Object.fromEntries(Object.entries(stored).filter((entry) => typeof entry[1] === 'string'));
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
async updatePluginSecrets(pluginId, values, removeKeys = []) {
|
|
321
|
+
await this.withLock(async () => {
|
|
322
|
+
const envelope = await this.readEnvelope();
|
|
323
|
+
const all = isRecord(envelope.pluginSecrets)
|
|
324
|
+
? { ...envelope.pluginSecrets }
|
|
325
|
+
: {};
|
|
326
|
+
const stored = all[pluginId];
|
|
327
|
+
const current = isRecord(stored)
|
|
328
|
+
? Object.fromEntries(Object.entries(stored).filter((entry) => typeof entry[1] === 'string'))
|
|
329
|
+
: {};
|
|
330
|
+
const before = JSON.stringify(current);
|
|
331
|
+
for (const key of removeKeys)
|
|
332
|
+
delete current[key];
|
|
333
|
+
Object.assign(current, values);
|
|
334
|
+
if (JSON.stringify(current) === before)
|
|
335
|
+
return;
|
|
336
|
+
if (Object.keys(current).length === 0)
|
|
337
|
+
delete all[pluginId];
|
|
338
|
+
else
|
|
339
|
+
all[pluginId] = current;
|
|
340
|
+
if (Object.keys(all).length === 0)
|
|
341
|
+
delete envelope.pluginSecrets;
|
|
342
|
+
else {
|
|
343
|
+
envelope.pluginSecrets = all;
|
|
344
|
+
}
|
|
345
|
+
await this.writeEnvelope(envelope);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
async updatePluginSecretsTransaction(updates, prepareCommit) {
|
|
349
|
+
await this.withLock(async () => {
|
|
350
|
+
const snapshot = await this.captureCredentialSnapshot();
|
|
351
|
+
const original = snapshot.envelope;
|
|
352
|
+
const all = isRecord(original.pluginSecrets)
|
|
353
|
+
? { ...original.pluginSecrets }
|
|
354
|
+
: {};
|
|
355
|
+
let changed = false;
|
|
356
|
+
for (const update of updates) {
|
|
357
|
+
const stored = all[update.pluginId];
|
|
358
|
+
const current = isRecord(stored)
|
|
359
|
+
? Object.fromEntries(Object.entries(stored).filter((entry) => typeof entry[1] === 'string'))
|
|
360
|
+
: {};
|
|
361
|
+
const before = JSON.stringify(current);
|
|
362
|
+
for (const key of update.removeKeys ?? [])
|
|
363
|
+
delete current[key];
|
|
364
|
+
Object.assign(current, update.values);
|
|
365
|
+
if (JSON.stringify(current) === before)
|
|
366
|
+
continue;
|
|
367
|
+
changed = true;
|
|
368
|
+
if (Object.keys(current).length === 0)
|
|
369
|
+
delete all[update.pluginId];
|
|
370
|
+
else
|
|
371
|
+
all[update.pluginId] = current;
|
|
372
|
+
}
|
|
373
|
+
const next = { ...original };
|
|
374
|
+
if (Object.keys(all).length === 0)
|
|
375
|
+
delete next.pluginSecrets;
|
|
376
|
+
else {
|
|
377
|
+
next.pluginSecrets = all;
|
|
378
|
+
}
|
|
379
|
+
const commit = await prepareCommit();
|
|
380
|
+
const journal = {
|
|
381
|
+
version: 1,
|
|
382
|
+
settingsPath: resolve(commit.path),
|
|
383
|
+
settingsBeforeHash: commit.beforeSource === undefined
|
|
384
|
+
? null
|
|
385
|
+
: sourceHash(commit.beforeSource),
|
|
386
|
+
settingsAfterHash: sourceHash(commit.afterSource),
|
|
387
|
+
snapshot,
|
|
388
|
+
nextEnvelope: next,
|
|
389
|
+
};
|
|
390
|
+
try {
|
|
391
|
+
if (changed) {
|
|
392
|
+
await this.writePluginSecretTransactionJournal(journal);
|
|
393
|
+
await this.writeEnvelope(next);
|
|
394
|
+
}
|
|
395
|
+
const committed = await writeFileAtomically(journal.settingsPath, commit.afterSource);
|
|
396
|
+
if (!committed)
|
|
397
|
+
throw new Error('Plugin settings write was interrupted');
|
|
398
|
+
if (changed)
|
|
399
|
+
await this.deletePluginSecretTransactionJournal();
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
if (changed) {
|
|
403
|
+
try {
|
|
404
|
+
await this.recoverPluginSecretTransaction(journal);
|
|
405
|
+
}
|
|
406
|
+
catch (rollbackError) {
|
|
407
|
+
throw new AggregateError([error, rollbackError], 'Plugin configuration failed and credential rollback failed');
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
async deletePluginSecrets(pluginId) {
|
|
415
|
+
await this.withLock(async () => {
|
|
416
|
+
const envelope = await this.readEnvelope();
|
|
417
|
+
if (!isRecord(envelope.pluginSecrets))
|
|
418
|
+
return;
|
|
419
|
+
const prefix = `${pluginId}/`;
|
|
420
|
+
const remaining = Object.fromEntries(Object.entries(envelope.pluginSecrets).filter(([key]) => key !== pluginId && !key.startsWith(prefix)));
|
|
421
|
+
if (Object.keys(remaining).length ===
|
|
422
|
+
Object.keys(envelope.pluginSecrets).length)
|
|
423
|
+
return;
|
|
424
|
+
if (Object.keys(remaining).length === 0)
|
|
425
|
+
delete envelope.pluginSecrets;
|
|
426
|
+
else {
|
|
427
|
+
envelope.pluginSecrets = remaining;
|
|
428
|
+
}
|
|
429
|
+
await this.writeEnvelope(envelope);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
async readEnvelope() {
|
|
433
|
+
if (this.useKeychain) {
|
|
434
|
+
const keychain = await readKeychainEnvelope(this.service);
|
|
435
|
+
if (keychain !== undefined)
|
|
436
|
+
return keychain;
|
|
437
|
+
}
|
|
438
|
+
return readFileEnvelope(credentialPath(this.configRoot));
|
|
439
|
+
}
|
|
440
|
+
pluginSecretTransactionPath() {
|
|
441
|
+
return join(this.configRoot, 'praxis', 'transactions', 'plugin-config.json');
|
|
442
|
+
}
|
|
443
|
+
pluginSecretTransactionService() {
|
|
444
|
+
return `${this.service}-plugin-config-transaction`;
|
|
445
|
+
}
|
|
446
|
+
async readPluginSecretTransactionJournal() {
|
|
447
|
+
if (this.useKeychain) {
|
|
448
|
+
const stored = await readKeychainEnvelope(this.pluginSecretTransactionService());
|
|
449
|
+
if (stored !== undefined)
|
|
450
|
+
return parsePluginSecretTransactionJournal(stored);
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
const source = await readFile(this.pluginSecretTransactionPath(), 'utf8');
|
|
454
|
+
if (Buffer.byteLength(source) > MAX_CREDENTIAL_BYTES) {
|
|
455
|
+
throw new Error('Plugin configuration transaction exceeds 2 MiB');
|
|
456
|
+
}
|
|
457
|
+
return parsePluginSecretTransactionJournal(JSON.parse(source));
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
if (error.code === 'ENOENT')
|
|
461
|
+
return undefined;
|
|
462
|
+
if (error instanceof SyntaxError) {
|
|
463
|
+
throw new Error('Invalid plugin configuration transaction journal', {
|
|
464
|
+
cause: error,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async writePluginSecretTransactionJournal(journal) {
|
|
471
|
+
const source = `${JSON.stringify(journal, null, 2)}\n`;
|
|
472
|
+
if (Buffer.byteLength(source) > MAX_CREDENTIAL_BYTES) {
|
|
473
|
+
throw new Error('Plugin configuration transaction exceeds 2 MiB');
|
|
474
|
+
}
|
|
475
|
+
if (this.useKeychain) {
|
|
476
|
+
try {
|
|
477
|
+
await writeKeychainEnvelope(this.pluginSecretTransactionService(), journal);
|
|
478
|
+
await rm(this.pluginSecretTransactionPath(), { force: true });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
if (error.code !== 'ENOENT')
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const committed = await writeFileAtomically(this.pluginSecretTransactionPath(), source);
|
|
487
|
+
if (!committed)
|
|
488
|
+
throw new Error('Plugin configuration transaction write was interrupted');
|
|
489
|
+
}
|
|
490
|
+
async deletePluginSecretTransactionJournal() {
|
|
491
|
+
const failures = [];
|
|
492
|
+
if (this.useKeychain) {
|
|
493
|
+
try {
|
|
494
|
+
await deleteKeychainEnvelope(this.pluginSecretTransactionService());
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
failures.push(error);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
await rm(this.pluginSecretTransactionPath(), { force: true });
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
failures.push(error);
|
|
505
|
+
}
|
|
506
|
+
if (failures.length === 1)
|
|
507
|
+
throw failures[0];
|
|
508
|
+
if (failures.length > 1) {
|
|
509
|
+
throw new AggregateError(failures, 'Plugin configuration transaction cleanup failed');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async recoverPluginSecretTransaction(knownJournal) {
|
|
513
|
+
const journal = knownJournal ?? (await this.readPluginSecretTransactionJournal());
|
|
514
|
+
if (journal === undefined)
|
|
515
|
+
return;
|
|
516
|
+
let settingsSource;
|
|
517
|
+
try {
|
|
518
|
+
settingsSource = await readFile(journal.settingsPath, 'utf8');
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
const code = error.code;
|
|
522
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR')
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
const settingsHash = settingsSource === undefined ? null : sourceHash(settingsSource);
|
|
526
|
+
if (settingsHash === journal.settingsAfterHash) {
|
|
527
|
+
await this.writeEnvelope(journal.nextEnvelope);
|
|
528
|
+
}
|
|
529
|
+
else if (settingsHash === journal.settingsBeforeHash) {
|
|
530
|
+
await this.restoreCredentialSnapshot(journal.snapshot);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
throw new Error(`Plugin configuration transaction conflicts with ${journal.settingsPath}`);
|
|
534
|
+
}
|
|
535
|
+
await this.deletePluginSecretTransactionJournal();
|
|
536
|
+
}
|
|
537
|
+
async captureCredentialSnapshot() {
|
|
538
|
+
const fileSource = await readFileEnvelopeSource(credentialPath(this.configRoot));
|
|
539
|
+
if (!this.useKeychain) {
|
|
540
|
+
return {
|
|
541
|
+
envelope: fileSource === undefined ? {} : parseEnvelope(fileSource),
|
|
542
|
+
...(fileSource === undefined ? {} : { fileSource }),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const keychainEnvelope = await readKeychainEnvelope(this.service);
|
|
546
|
+
return {
|
|
547
|
+
envelope: keychainEnvelope ??
|
|
548
|
+
(fileSource === undefined ? {} : parseEnvelope(fileSource)),
|
|
549
|
+
...(fileSource === undefined ? {} : { fileSource }),
|
|
550
|
+
...(keychainEnvelope === undefined ? {} : { keychainEnvelope }),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async restoreCredentialSnapshot(snapshot) {
|
|
554
|
+
const failures = [];
|
|
555
|
+
if (this.useKeychain) {
|
|
556
|
+
try {
|
|
557
|
+
if (snapshot.keychainEnvelope === undefined) {
|
|
558
|
+
await deleteKeychainEnvelope(this.service);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
await writeKeychainEnvelope(this.service, snapshot.keychainEnvelope);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
failures.push(error);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
const path = credentialPath(this.configRoot);
|
|
570
|
+
if (snapshot.fileSource === undefined) {
|
|
571
|
+
await rm(path, { force: true });
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
const committed = await writeFileAtomically(path, snapshot.fileSource);
|
|
575
|
+
if (!committed)
|
|
576
|
+
throw new Error('MCP OAuth credential rollback was interrupted');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
failures.push(error);
|
|
581
|
+
}
|
|
582
|
+
if (failures.length === 1)
|
|
583
|
+
throw failures[0];
|
|
584
|
+
if (failures.length > 1)
|
|
585
|
+
throw new AggregateError(failures, 'Credential rollback failed');
|
|
586
|
+
}
|
|
587
|
+
async writeEnvelope(envelope) {
|
|
588
|
+
if (this.useKeychain) {
|
|
589
|
+
try {
|
|
590
|
+
await writeKeychainEnvelope(this.service, envelope);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
if (error.code !== 'ENOENT')
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const committed = await writeFileAtomically(credentialPath(this.configRoot), `${JSON.stringify(envelope, null, 2)}\n`);
|
|
599
|
+
if (!committed)
|
|
600
|
+
throw new Error('MCP OAuth credential write was interrupted');
|
|
601
|
+
}
|
|
602
|
+
async withLock(operation) {
|
|
603
|
+
const started = Date.now();
|
|
604
|
+
let lock = await this.lease.tryAcquire();
|
|
605
|
+
while (!lock) {
|
|
606
|
+
if (Date.now() - started >= LOCK_WAIT_MS) {
|
|
607
|
+
throw new Error('MCP OAuth credential store is locked');
|
|
608
|
+
}
|
|
609
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
|
|
610
|
+
lock = await this.lease.tryAcquire();
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
await this.recoverPluginSecretTransaction();
|
|
614
|
+
return await operation();
|
|
615
|
+
}
|
|
616
|
+
finally {
|
|
617
|
+
await lock.release();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function clientMetadata(serverName, redirectUri) {
|
|
622
|
+
return {
|
|
623
|
+
client_name: `Praxis (${serverName})`,
|
|
624
|
+
redirect_uris: [redirectUri],
|
|
625
|
+
response_types: ['code'],
|
|
626
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
627
|
+
token_endpoint_auth_method: 'none',
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
export async function readMcpClientSecret() {
|
|
631
|
+
const envSecret = process.env.MCP_CLIENT_SECRET;
|
|
632
|
+
if (envSecret)
|
|
633
|
+
return envSecret;
|
|
634
|
+
if (!process.stdin.isTTY) {
|
|
635
|
+
throw new Error('No TTY available to prompt for client secret. Set MCP_CLIENT_SECRET env var instead.');
|
|
636
|
+
}
|
|
637
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
638
|
+
process.stderr.write('Enter OAuth client secret: ');
|
|
639
|
+
process.stdin.setRawMode?.(true);
|
|
640
|
+
let secret = '';
|
|
641
|
+
const onData = (chunk) => {
|
|
642
|
+
const value = chunk.toString();
|
|
643
|
+
if (value === '\n' || value === '\r') {
|
|
644
|
+
process.stdin.setRawMode?.(false);
|
|
645
|
+
process.stdin.removeListener('data', onData);
|
|
646
|
+
process.stderr.write('\n');
|
|
647
|
+
resolvePromise(secret);
|
|
648
|
+
}
|
|
649
|
+
else if (value === '\u0003') {
|
|
650
|
+
process.stdin.setRawMode?.(false);
|
|
651
|
+
process.stdin.removeListener('data', onData);
|
|
652
|
+
rejectPromise(new Error('Cancelled'));
|
|
653
|
+
}
|
|
654
|
+
else if (value === '\u007f' || value === '\b') {
|
|
655
|
+
secret = secret.slice(0, -1);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
secret += value;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
process.stdin.on('data', onData);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
export class ClaudeMcpOAuthProvider {
|
|
665
|
+
store;
|
|
666
|
+
server;
|
|
667
|
+
redirectUri;
|
|
668
|
+
onAuthorization;
|
|
669
|
+
configuredClient;
|
|
670
|
+
codeVerifierValue;
|
|
671
|
+
stateValue;
|
|
672
|
+
recordValue;
|
|
673
|
+
constructor(store, server, redirectUri, onAuthorization, record, configuredClient) {
|
|
674
|
+
this.store = store;
|
|
675
|
+
this.server = server;
|
|
676
|
+
this.redirectUri = redirectUri;
|
|
677
|
+
this.onAuthorization = onAuthorization;
|
|
678
|
+
this.configuredClient = configuredClient;
|
|
679
|
+
this.recordValue = record;
|
|
680
|
+
}
|
|
681
|
+
get redirectUrl() {
|
|
682
|
+
return this.redirectUri;
|
|
683
|
+
}
|
|
684
|
+
get clientMetadata() {
|
|
685
|
+
return clientMetadata(this.server.name, this.redirectUri);
|
|
686
|
+
}
|
|
687
|
+
async state() {
|
|
688
|
+
this.stateValue ??= randomBytes(32).toString('base64url');
|
|
689
|
+
return this.stateValue;
|
|
690
|
+
}
|
|
691
|
+
authorizationState() {
|
|
692
|
+
if (!this.stateValue)
|
|
693
|
+
throw new Error('MCP OAuth authorization state is unavailable');
|
|
694
|
+
return this.stateValue;
|
|
695
|
+
}
|
|
696
|
+
clientInformation() {
|
|
697
|
+
if (this.recordValue?.clientId &&
|
|
698
|
+
this.recordValue.redirectUri === this.redirectUri) {
|
|
699
|
+
return {
|
|
700
|
+
client_id: this.recordValue.clientId,
|
|
701
|
+
...(this.recordValue.clientSecret
|
|
702
|
+
? { client_secret: this.recordValue.clientSecret }
|
|
703
|
+
: {}),
|
|
704
|
+
redirect_uris: [this.recordValue.redirectUri ?? this.redirectUri],
|
|
705
|
+
token_endpoint_auth_method: 'none',
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
if (!this.configuredClient)
|
|
709
|
+
return undefined;
|
|
710
|
+
return {
|
|
711
|
+
client_id: this.configuredClient.clientId,
|
|
712
|
+
...(this.configuredClient.clientSecret
|
|
713
|
+
? { client_secret: this.configuredClient.clientSecret }
|
|
714
|
+
: {}),
|
|
715
|
+
redirect_uris: [this.redirectUri],
|
|
716
|
+
token_endpoint_auth_method: 'none',
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
async saveClientInformation(information) {
|
|
720
|
+
if (typeof information.client_id !== 'string') {
|
|
721
|
+
throw new Error('MCP OAuth client registration returned no client_id');
|
|
722
|
+
}
|
|
723
|
+
this.recordValue = await this.store.mutate(this.server, (current) => ({
|
|
724
|
+
serverName: this.server.name,
|
|
725
|
+
serverUrl: this.server.url,
|
|
726
|
+
accessToken: current?.accessToken ?? '',
|
|
727
|
+
...(current?.discoveryState === undefined
|
|
728
|
+
? {}
|
|
729
|
+
: { discoveryState: current.discoveryState }),
|
|
730
|
+
clientId: information.client_id,
|
|
731
|
+
...(typeof information.client_secret === 'string'
|
|
732
|
+
? { clientSecret: information.client_secret }
|
|
733
|
+
: {}),
|
|
734
|
+
redirectUri: this.redirectUri,
|
|
735
|
+
...(current?.refreshToken ? { refreshToken: current.refreshToken } : {}),
|
|
736
|
+
...(current?.expiresAt === undefined
|
|
737
|
+
? {}
|
|
738
|
+
: { expiresAt: current.expiresAt }),
|
|
739
|
+
...(current?.scope ? { scope: current.scope } : {}),
|
|
740
|
+
}));
|
|
741
|
+
}
|
|
742
|
+
async tokens() {
|
|
743
|
+
const record = this.recordValue ?? (await this.store.read(this.server));
|
|
744
|
+
this.recordValue = record;
|
|
745
|
+
if (!record?.accessToken)
|
|
746
|
+
return undefined;
|
|
747
|
+
const expiresIn = record.expiresAt === undefined
|
|
748
|
+
? undefined
|
|
749
|
+
: Math.max(0, Math.floor((record.expiresAt - Date.now()) / 1000));
|
|
750
|
+
return {
|
|
751
|
+
access_token: record.accessToken,
|
|
752
|
+
token_type: 'Bearer',
|
|
753
|
+
...(record.refreshToken ? { refresh_token: record.refreshToken } : {}),
|
|
754
|
+
...(expiresIn === undefined ? {} : { expires_in: expiresIn }),
|
|
755
|
+
...(record.scope ? { scope: record.scope } : {}),
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
async saveTokens(tokens) {
|
|
759
|
+
if (!tokens.access_token)
|
|
760
|
+
throw new Error('MCP OAuth token response had no access_token');
|
|
761
|
+
const expiresAt = typeof tokens.expires_in === 'number'
|
|
762
|
+
? Date.now() + Math.max(0, tokens.expires_in) * 1000
|
|
763
|
+
: undefined;
|
|
764
|
+
this.recordValue = await this.store.mutate(this.server, (current) => ({
|
|
765
|
+
serverName: this.server.name,
|
|
766
|
+
serverUrl: this.server.url,
|
|
767
|
+
accessToken: tokens.access_token,
|
|
768
|
+
...(current?.discoveryState === undefined
|
|
769
|
+
? {}
|
|
770
|
+
: { discoveryState: current.discoveryState }),
|
|
771
|
+
...(current?.clientId ? { clientId: current.clientId } : {}),
|
|
772
|
+
...(current?.clientSecret ? { clientSecret: current.clientSecret } : {}),
|
|
773
|
+
redirectUri: current?.redirectUri ?? this.redirectUri,
|
|
774
|
+
...(tokens.refresh_token
|
|
775
|
+
? { refreshToken: tokens.refresh_token }
|
|
776
|
+
: current?.refreshToken
|
|
777
|
+
? { refreshToken: current.refreshToken }
|
|
778
|
+
: {}),
|
|
779
|
+
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
780
|
+
...(tokens.scope
|
|
781
|
+
? { scope: tokens.scope }
|
|
782
|
+
: current?.scope
|
|
783
|
+
? { scope: current.scope }
|
|
784
|
+
: {}),
|
|
785
|
+
}));
|
|
786
|
+
}
|
|
787
|
+
async redirectToAuthorization(url) {
|
|
788
|
+
await this.onAuthorization(url);
|
|
789
|
+
}
|
|
790
|
+
saveCodeVerifier(codeVerifier) {
|
|
791
|
+
this.codeVerifierValue = codeVerifier;
|
|
792
|
+
}
|
|
793
|
+
codeVerifier() {
|
|
794
|
+
if (!this.codeVerifierValue)
|
|
795
|
+
throw new Error('MCP OAuth code verifier is unavailable');
|
|
796
|
+
return this.codeVerifierValue;
|
|
797
|
+
}
|
|
798
|
+
async saveDiscoveryState(state) {
|
|
799
|
+
this.recordValue = await this.store.mutate(this.server, (current) => ({
|
|
800
|
+
serverName: this.server.name,
|
|
801
|
+
serverUrl: this.server.url,
|
|
802
|
+
accessToken: current?.accessToken ?? '',
|
|
803
|
+
discoveryState: {
|
|
804
|
+
...(state.authorizationServerUrl
|
|
805
|
+
? { authorizationServerUrl: String(state.authorizationServerUrl) }
|
|
806
|
+
: {}),
|
|
807
|
+
oauthMetadataFound: state.authorizationServerMetadata !== undefined,
|
|
808
|
+
},
|
|
809
|
+
...(current?.clientId ? { clientId: current.clientId } : {}),
|
|
810
|
+
...(current?.clientSecret ? { clientSecret: current.clientSecret } : {}),
|
|
811
|
+
...(current?.redirectUri
|
|
812
|
+
? { redirectUri: current.redirectUri }
|
|
813
|
+
: { redirectUri: this.redirectUri }),
|
|
814
|
+
...(current?.refreshToken ? { refreshToken: current.refreshToken } : {}),
|
|
815
|
+
...(current?.expiresAt === undefined
|
|
816
|
+
? {}
|
|
817
|
+
: { expiresAt: current.expiresAt }),
|
|
818
|
+
...(current?.scope ? { scope: current.scope } : {}),
|
|
819
|
+
}));
|
|
820
|
+
}
|
|
821
|
+
async discoveryState() {
|
|
822
|
+
const state = this.recordValue?.discoveryState;
|
|
823
|
+
if (!state?.authorizationServerUrl)
|
|
824
|
+
return undefined;
|
|
825
|
+
return { authorizationServerUrl: state.authorizationServerUrl };
|
|
826
|
+
}
|
|
827
|
+
async invalidateCredentials(scope) {
|
|
828
|
+
if (scope === 'all') {
|
|
829
|
+
this.recordValue = undefined;
|
|
830
|
+
await this.store.clear(this.server);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
this.recordValue = await this.store.mutate(this.server, (current) => {
|
|
834
|
+
if (!current)
|
|
835
|
+
return undefined;
|
|
836
|
+
if (scope === 'tokens') {
|
|
837
|
+
const next = { ...current, accessToken: '' };
|
|
838
|
+
delete next.refreshToken;
|
|
839
|
+
delete next.expiresAt;
|
|
840
|
+
delete next.scope;
|
|
841
|
+
return next;
|
|
842
|
+
}
|
|
843
|
+
if (scope === 'client') {
|
|
844
|
+
const next = { ...current };
|
|
845
|
+
delete next.clientId;
|
|
846
|
+
delete next.clientSecret;
|
|
847
|
+
return next;
|
|
848
|
+
}
|
|
849
|
+
if (scope === 'discovery') {
|
|
850
|
+
const next = { ...current };
|
|
851
|
+
delete next.discoveryState;
|
|
852
|
+
return next;
|
|
853
|
+
}
|
|
854
|
+
if (scope === 'verifier')
|
|
855
|
+
this.codeVerifierValue = undefined;
|
|
856
|
+
return current;
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
export function mcpOAuthServerIdentity(name, config) {
|
|
861
|
+
const type = config.type === 'sse'
|
|
862
|
+
? 'sse'
|
|
863
|
+
: config.type === 'http' || config.url !== undefined
|
|
864
|
+
? 'http'
|
|
865
|
+
: undefined;
|
|
866
|
+
if (!type || typeof config.url !== 'string' || config.url.length === 0) {
|
|
867
|
+
throw new Error(`MCP server ${name} must use HTTP or SSE transport for OAuth`);
|
|
868
|
+
}
|
|
869
|
+
const headers = isStringRecord(config.headers) ? config.headers : undefined;
|
|
870
|
+
const oauth = isRecord(config.oauth) ? config.oauth : undefined;
|
|
871
|
+
const clientId = stringValue(oauth?.clientId);
|
|
872
|
+
const callbackPort = typeof oauth?.callbackPort === 'number' &&
|
|
873
|
+
Number.isFinite(oauth.callbackPort)
|
|
874
|
+
? oauth.callbackPort
|
|
875
|
+
: undefined;
|
|
876
|
+
return {
|
|
877
|
+
name,
|
|
878
|
+
type,
|
|
879
|
+
url: config.url,
|
|
880
|
+
...(headers ? { headers } : {}),
|
|
881
|
+
...(clientId ? { clientId } : {}),
|
|
882
|
+
...(callbackPort === undefined ? {} : { callbackPort }),
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function isStringRecord(value) {
|
|
886
|
+
return (isRecord(value) &&
|
|
887
|
+
Object.values(value).every((item) => typeof item === 'string'));
|
|
888
|
+
}
|
|
889
|
+
async function listenCallbackServer(port = 0) {
|
|
890
|
+
let callbackOrigin = 'http://localhost';
|
|
891
|
+
let resolveCallback = () => undefined;
|
|
892
|
+
let rejectCallback = () => undefined;
|
|
893
|
+
const callback = new Promise((resolvePromise, rejectPromise) => {
|
|
894
|
+
resolveCallback = resolvePromise;
|
|
895
|
+
rejectCallback = rejectPromise;
|
|
896
|
+
});
|
|
897
|
+
const server = createServer((request, response) => {
|
|
898
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
899
|
+
if (url.pathname !== '/callback') {
|
|
900
|
+
response.writeHead(404);
|
|
901
|
+
response.end('Not found');
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const callbackUrl = `${callbackOrigin}${url.pathname}${url.search}`;
|
|
905
|
+
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
906
|
+
response.end('<html><body>Praxis authorization received. You may close this tab.</body></html>');
|
|
907
|
+
resolveCallback(callbackUrl);
|
|
908
|
+
});
|
|
909
|
+
server.on('error', (error) => rejectCallback(error));
|
|
910
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
911
|
+
server.once('listening', () => resolvePromise());
|
|
912
|
+
server.once('error', rejectPromise);
|
|
913
|
+
server.listen(port, '127.0.0.1');
|
|
914
|
+
});
|
|
915
|
+
const address = server.address();
|
|
916
|
+
if (!address || typeof address === 'string') {
|
|
917
|
+
await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
918
|
+
throw new Error('MCP OAuth callback server did not expose a port');
|
|
919
|
+
}
|
|
920
|
+
callbackOrigin = `http://localhost:${address.port}`;
|
|
921
|
+
return {
|
|
922
|
+
server,
|
|
923
|
+
redirectUri: `http://localhost:${address.port}/callback`,
|
|
924
|
+
wait: () => callback,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
async function closeServer(server) {
|
|
928
|
+
await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
929
|
+
}
|
|
930
|
+
async function defaultOpenBrowser(url) {
|
|
931
|
+
const command = process.platform === 'darwin'
|
|
932
|
+
? 'open'
|
|
933
|
+
: process.platform === 'win32'
|
|
934
|
+
? 'cmd'
|
|
935
|
+
: 'xdg-open';
|
|
936
|
+
const args = process.platform === 'win32'
|
|
937
|
+
? ['/c', 'start', '', url.toString()]
|
|
938
|
+
: [url.toString()];
|
|
939
|
+
await execFileAsync(command, args, { timeout: 10_000 }).catch(() => undefined);
|
|
940
|
+
}
|
|
941
|
+
async function firstInputLine(readRedirectUrl) {
|
|
942
|
+
const value = await readRedirectUrl();
|
|
943
|
+
if (value.trim().length === 0)
|
|
944
|
+
throw new Error('MCP OAuth redirect URL is required');
|
|
945
|
+
return value.trim();
|
|
946
|
+
}
|
|
947
|
+
function callbackParameter(url, name) {
|
|
948
|
+
const values = url.searchParams.getAll(name);
|
|
949
|
+
if (values.length > 1) {
|
|
950
|
+
throw new Error(`MCP OAuth redirect URL contained multiple ${name} values`);
|
|
951
|
+
}
|
|
952
|
+
return values[0];
|
|
953
|
+
}
|
|
954
|
+
function validateAuthorizationCallback(value, redirectUri, expectedState) {
|
|
955
|
+
let actual;
|
|
956
|
+
try {
|
|
957
|
+
actual = new URL(value);
|
|
958
|
+
}
|
|
959
|
+
catch (error) {
|
|
960
|
+
throw new Error('MCP OAuth redirect URL is invalid', { cause: error });
|
|
961
|
+
}
|
|
962
|
+
const expected = new URL(redirectUri);
|
|
963
|
+
if (actual.origin !== expected.origin ||
|
|
964
|
+
actual.pathname !== expected.pathname) {
|
|
965
|
+
throw new Error('MCP OAuth redirect URL does not match the local callback');
|
|
966
|
+
}
|
|
967
|
+
const state = callbackParameter(actual, 'state');
|
|
968
|
+
if (!state || state !== expectedState) {
|
|
969
|
+
throw new Error('MCP OAuth redirect state did not match the authorization request');
|
|
970
|
+
}
|
|
971
|
+
return actual;
|
|
972
|
+
}
|
|
973
|
+
export async function authenticateMcpServer(options) {
|
|
974
|
+
const callback = await listenCallbackServer(options.callbackPort ?? options.server.callbackPort);
|
|
975
|
+
const store = new ClaudeMcpOAuthStore({ configRoot: options.configRoot });
|
|
976
|
+
const existing = await store.read(options.server);
|
|
977
|
+
const clientId = options.clientId ?? options.server.clientId;
|
|
978
|
+
const clientSecret = options.clientSecret ??
|
|
979
|
+
(clientId ? await store.readClientSecret(options.server) : undefined);
|
|
980
|
+
const provider = new ClaudeMcpOAuthProvider(store, options.server, callback.redirectUri, async (url) => {
|
|
981
|
+
options.write(`Visit this URL to authorize:\n ${url}\n`);
|
|
982
|
+
if (!options.noBrowser) {
|
|
983
|
+
await (options.openBrowser ?? defaultOpenBrowser)(url);
|
|
984
|
+
}
|
|
985
|
+
}, existing, clientId
|
|
986
|
+
? {
|
|
987
|
+
clientId,
|
|
988
|
+
...(clientSecret ? { clientSecret } : {}),
|
|
989
|
+
}
|
|
990
|
+
: undefined);
|
|
991
|
+
try {
|
|
992
|
+
const result = await auth(provider, { serverUrl: options.server.url });
|
|
993
|
+
if (result === 'REDIRECT') {
|
|
994
|
+
const input = options.noBrowser
|
|
995
|
+
? options.readRedirectUrl
|
|
996
|
+
? firstInputLine(options.readRedirectUrl)
|
|
997
|
+
: callback.wait()
|
|
998
|
+
: callback.wait();
|
|
999
|
+
let timeout;
|
|
1000
|
+
let redirectUrl;
|
|
1001
|
+
try {
|
|
1002
|
+
redirectUrl = await Promise.race([
|
|
1003
|
+
input,
|
|
1004
|
+
new Promise((_, reject) => {
|
|
1005
|
+
timeout = setTimeout(() => reject(new Error('MCP OAuth authorization timed out')), LOGIN_TIMEOUT_MS);
|
|
1006
|
+
}),
|
|
1007
|
+
]);
|
|
1008
|
+
}
|
|
1009
|
+
finally {
|
|
1010
|
+
if (timeout)
|
|
1011
|
+
clearTimeout(timeout);
|
|
1012
|
+
}
|
|
1013
|
+
const parsed = validateAuthorizationCallback(redirectUrl, callback.redirectUri, provider.authorizationState());
|
|
1014
|
+
const error = callbackParameter(parsed, 'error');
|
|
1015
|
+
if (error) {
|
|
1016
|
+
throw new Error(callbackParameter(parsed, 'error_description') ?? error);
|
|
1017
|
+
}
|
|
1018
|
+
const code = callbackParameter(parsed, 'code');
|
|
1019
|
+
if (!code)
|
|
1020
|
+
throw new Error('MCP OAuth redirect URL did not contain a code');
|
|
1021
|
+
const completed = await auth(provider, {
|
|
1022
|
+
serverUrl: options.server.url,
|
|
1023
|
+
authorizationCode: code,
|
|
1024
|
+
});
|
|
1025
|
+
if (completed !== 'AUTHORIZED') {
|
|
1026
|
+
throw new Error('MCP OAuth token exchange did not complete');
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return 'AUTHORIZED';
|
|
1030
|
+
}
|
|
1031
|
+
finally {
|
|
1032
|
+
await closeServer(callback.server);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
export async function loadMcpOAuthProvider(configRoot, server) {
|
|
1036
|
+
const store = new ClaudeMcpOAuthStore({ configRoot });
|
|
1037
|
+
const record = await store.read(server);
|
|
1038
|
+
if (!record || (!record.accessToken && !record.refreshToken))
|
|
1039
|
+
return undefined;
|
|
1040
|
+
const redirectUri = record.redirectUri ?? 'http://localhost:0/callback';
|
|
1041
|
+
const clientSecret = server.clientId
|
|
1042
|
+
? await store.readClientSecret(server)
|
|
1043
|
+
: undefined;
|
|
1044
|
+
return new ClaudeMcpOAuthProvider(store, server, redirectUri, async () => {
|
|
1045
|
+
throw new Error(`MCP server ${server.name} requires authentication; run praxis mcp login ${server.name}`);
|
|
1046
|
+
}, record, server.clientId
|
|
1047
|
+
? {
|
|
1048
|
+
clientId: server.clientId,
|
|
1049
|
+
...(clientSecret ? { clientSecret } : {}),
|
|
1050
|
+
}
|
|
1051
|
+
: undefined);
|
|
1052
|
+
}
|
|
1053
|
+
//# sourceMappingURL=claude-mcp-oauth.js.map
|