patchwork-os 0.2.0-alpha.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.bridge.md +352 -0
- package/README.md +72 -0
- package/deploy/README.md +172 -0
- package/deploy/bootstrap-new-vps.sh +364 -0
- package/deploy/claude-ide-bridge.service.template +67 -0
- package/deploy/claude-ide-bridge@.service +31 -0
- package/deploy/ecosystem.config.js.example +36 -0
- package/deploy/install-vps-service.sh +240 -0
- package/deploy/nginx-claude-bridge.conf.template +129 -0
- package/dist/activityLog.d.ts +112 -0
- package/dist/activityLog.js +399 -0
- package/dist/activityLog.js.map +1 -0
- package/dist/activityTypes.d.ts +28 -0
- package/dist/activityTypes.js +9 -0
- package/dist/activityTypes.js.map +1 -0
- package/dist/adapters/base.d.ts +78 -0
- package/dist/adapters/base.js +14 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/claude.d.ts +18 -0
- package/dist/adapters/claude.js +276 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/gemini.d.ts +17 -0
- package/dist/adapters/gemini.js +218 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/grok.d.ts +7 -0
- package/dist/adapters/grok.js +21 -0
- package/dist/adapters/grok.js.map +1 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.js +37 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/local.d.ts +7 -0
- package/dist/adapters/local.js +22 -0
- package/dist/adapters/local.js.map +1 -0
- package/dist/adapters/openai.d.ts +22 -0
- package/dist/adapters/openai.js +284 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/adapters/sse.d.ts +13 -0
- package/dist/adapters/sse.js +58 -0
- package/dist/adapters/sse.js.map +1 -0
- package/dist/analyticsAggregator.d.ts +28 -0
- package/dist/analyticsAggregator.js +133 -0
- package/dist/analyticsAggregator.js.map +1 -0
- package/dist/analyticsPrefs.d.ts +9 -0
- package/dist/analyticsPrefs.js +50 -0
- package/dist/analyticsPrefs.js.map +1 -0
- package/dist/analyticsSend.d.ts +12 -0
- package/dist/analyticsSend.js +34 -0
- package/dist/analyticsSend.js.map +1 -0
- package/dist/approvalHttp.d.ts +46 -0
- package/dist/approvalHttp.js +370 -0
- package/dist/approvalHttp.js.map +1 -0
- package/dist/approvalQueue.d.ts +49 -0
- package/dist/approvalQueue.js +84 -0
- package/dist/approvalQueue.js.map +1 -0
- package/dist/automation.d.ts +675 -0
- package/dist/automation.js +1038 -0
- package/dist/automation.js.map +1 -0
- package/dist/bridge.d.ts +85 -0
- package/dist/bridge.js +1535 -0
- package/dist/bridge.js.map +1 -0
- package/dist/bridgeLockDiscovery.d.ts +11 -0
- package/dist/bridgeLockDiscovery.js +49 -0
- package/dist/bridgeLockDiscovery.js.map +1 -0
- package/dist/bridgeToken.d.ts +22 -0
- package/dist/bridgeToken.js +114 -0
- package/dist/bridgeToken.js.map +1 -0
- package/dist/bridgeToolsRules.d.ts +20 -0
- package/dist/bridgeToolsRules.js +79 -0
- package/dist/bridgeToolsRules.js.map +1 -0
- package/dist/ccPermissions.d.ts +59 -0
- package/dist/ccPermissions.js +163 -0
- package/dist/ccPermissions.js.map +1 -0
- package/dist/claudeDriver.d.ts +129 -0
- package/dist/claudeDriver.js +459 -0
- package/dist/claudeDriver.js.map +1 -0
- package/dist/claudeMdPatch.d.ts +29 -0
- package/dist/claudeMdPatch.js +164 -0
- package/dist/claudeMdPatch.js.map +1 -0
- package/dist/claudeOrchestrator.d.ts +171 -0
- package/dist/claudeOrchestrator.js +591 -0
- package/dist/claudeOrchestrator.js.map +1 -0
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.js +158 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/marketplace.d.ts +11 -0
- package/dist/commands/marketplace.js +120 -0
- package/dist/commands/marketplace.js.map +1 -0
- package/dist/commands/patchworkInit.d.ts +14 -0
- package/dist/commands/patchworkInit.js +155 -0
- package/dist/commands/patchworkInit.js.map +1 -0
- package/dist/commands/task.d.ts +14 -0
- package/dist/commands/task.js +289 -0
- package/dist/commands/task.js.map +1 -0
- package/dist/commands/tokenEfficiency.d.ts +9 -0
- package/dist/commands/tokenEfficiency.js +211 -0
- package/dist/commands/tokenEfficiency.js.map +1 -0
- package/dist/commands/tools.d.ts +28 -0
- package/dist/commands/tools.js +326 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/commitIssueLinkLog.d.ts +77 -0
- package/dist/commitIssueLinkLog.js +142 -0
- package/dist/commitIssueLinkLog.js.map +1 -0
- package/dist/companions/registry.d.ts +12 -0
- package/dist/companions/registry.js +71 -0
- package/dist/companions/registry.js.map +1 -0
- package/dist/config.d.ts +105 -0
- package/dist/config.js +720 -0
- package/dist/config.js.map +1 -0
- package/dist/crypto.d.ts +16 -0
- package/dist/crypto.js +34 -0
- package/dist/crypto.js.map +1 -0
- package/dist/dashboard.d.ts +12 -0
- package/dist/dashboard.js +149 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/decisionTraceLog.d.ts +77 -0
- package/dist/decisionTraceLog.js +147 -0
- package/dist/decisionTraceLog.js.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.js +34 -0
- package/dist/errors.js.map +1 -0
- package/dist/extensionClient.d.ts +279 -0
- package/dist/extensionClient.js +1253 -0
- package/dist/extensionClient.js.map +1 -0
- package/dist/fileLock.d.ts +36 -0
- package/dist/fileLock.js +121 -0
- package/dist/fileLock.js.map +1 -0
- package/dist/fp/activityAnalytics.d.ts +39 -0
- package/dist/fp/activityAnalytics.js +111 -0
- package/dist/fp/activityAnalytics.js.map +1 -0
- package/dist/fp/async.d.ts +48 -0
- package/dist/fp/async.js +60 -0
- package/dist/fp/async.js.map +1 -0
- package/dist/fp/automationInterpreter.d.ts +37 -0
- package/dist/fp/automationInterpreter.js +523 -0
- package/dist/fp/automationInterpreter.js.map +1 -0
- package/dist/fp/automationProgram.d.ts +89 -0
- package/dist/fp/automationProgram.js +29 -0
- package/dist/fp/automationProgram.js.map +1 -0
- package/dist/fp/automationState.d.ts +135 -0
- package/dist/fp/automationState.js +206 -0
- package/dist/fp/automationState.js.map +1 -0
- package/dist/fp/automationUtils.d.ts +31 -0
- package/dist/fp/automationUtils.js +61 -0
- package/dist/fp/automationUtils.js.map +1 -0
- package/dist/fp/brandedTypes.d.ts +32 -0
- package/dist/fp/brandedTypes.js +41 -0
- package/dist/fp/brandedTypes.js.map +1 -0
- package/dist/fp/commandDescription.d.ts +18 -0
- package/dist/fp/commandDescription.js +125 -0
- package/dist/fp/commandDescription.js.map +1 -0
- package/dist/fp/extensionSnapshot.d.ts +10 -0
- package/dist/fp/extensionSnapshot.js +14 -0
- package/dist/fp/extensionSnapshot.js.map +1 -0
- package/dist/fp/index.d.ts +8 -0
- package/dist/fp/index.js +9 -0
- package/dist/fp/index.js.map +1 -0
- package/dist/fp/interpreterContext.d.ts +69 -0
- package/dist/fp/interpreterContext.js +56 -0
- package/dist/fp/interpreterContext.js.map +1 -0
- package/dist/fp/policyParser.d.ts +16 -0
- package/dist/fp/policyParser.js +334 -0
- package/dist/fp/policyParser.js.map +1 -0
- package/dist/fp/result.d.ts +38 -0
- package/dist/fp/result.js +57 -0
- package/dist/fp/result.js.map +1 -0
- package/dist/fp/tokenBucket.d.ts +27 -0
- package/dist/fp/tokenBucket.js +36 -0
- package/dist/fp/tokenBucket.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1465 -0
- package/dist/index.js.map +1 -0
- package/dist/instructionsUtils.d.ts +17 -0
- package/dist/instructionsUtils.js +38 -0
- package/dist/instructionsUtils.js.map +1 -0
- package/dist/lockfile.d.ts +16 -0
- package/dist/lockfile.js +172 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +68 -0
- package/dist/logger.js.map +1 -0
- package/dist/oauth.d.ts +105 -0
- package/dist/oauth.js +880 -0
- package/dist/oauth.js.map +1 -0
- package/dist/orchestrator/childBridgeClient.d.ts +33 -0
- package/dist/orchestrator/childBridgeClient.js +321 -0
- package/dist/orchestrator/childBridgeClient.js.map +1 -0
- package/dist/orchestrator/childBridgeRegistry.d.ts +67 -0
- package/dist/orchestrator/childBridgeRegistry.js +297 -0
- package/dist/orchestrator/childBridgeRegistry.js.map +1 -0
- package/dist/orchestrator/index.d.ts +3 -0
- package/dist/orchestrator/index.js +3 -0
- package/dist/orchestrator/index.js.map +1 -0
- package/dist/orchestrator/orchestratorBridge.d.ts +32 -0
- package/dist/orchestrator/orchestratorBridge.js +412 -0
- package/dist/orchestrator/orchestratorBridge.js.map +1 -0
- package/dist/orchestrator/orchestratorConfig.d.ts +11 -0
- package/dist/orchestrator/orchestratorConfig.js +85 -0
- package/dist/orchestrator/orchestratorConfig.js.map +1 -0
- package/dist/orchestrator/orchestratorTools.d.ts +16 -0
- package/dist/orchestrator/orchestratorTools.js +272 -0
- package/dist/orchestrator/orchestratorTools.js.map +1 -0
- package/dist/patchworkCli.d.ts +15 -0
- package/dist/patchworkCli.js +41 -0
- package/dist/patchworkCli.js.map +1 -0
- package/dist/patchworkConfig.d.ts +28 -0
- package/dist/patchworkConfig.js +30 -0
- package/dist/patchworkConfig.js.map +1 -0
- package/dist/plugin.d.ts +106 -0
- package/dist/plugin.js +31 -0
- package/dist/plugin.js.map +1 -0
- package/dist/pluginLoader.d.ts +44 -0
- package/dist/pluginLoader.js +357 -0
- package/dist/pluginLoader.js.map +1 -0
- package/dist/pluginWatcher.d.ts +24 -0
- package/dist/pluginWatcher.js +139 -0
- package/dist/pluginWatcher.js.map +1 -0
- package/dist/preToolUseHook.d.ts +10 -0
- package/dist/preToolUseHook.js +57 -0
- package/dist/preToolUseHook.js.map +1 -0
- package/dist/probe.d.ts +35 -0
- package/dist/probe.js +143 -0
- package/dist/probe.js.map +1 -0
- package/dist/prompts.d.ts +27 -0
- package/dist/prompts.js +1680 -0
- package/dist/prompts.js.map +1 -0
- package/dist/quickTaskPresets.d.ts +64 -0
- package/dist/quickTaskPresets.js +156 -0
- package/dist/quickTaskPresets.js.map +1 -0
- package/dist/recipes/compiler.d.ts +44 -0
- package/dist/recipes/compiler.js +140 -0
- package/dist/recipes/compiler.js.map +1 -0
- package/dist/recipes/installer.d.ts +25 -0
- package/dist/recipes/installer.js +62 -0
- package/dist/recipes/installer.js.map +1 -0
- package/dist/recipes/parser.d.ts +18 -0
- package/dist/recipes/parser.js +160 -0
- package/dist/recipes/parser.js.map +1 -0
- package/dist/recipes/scheduler.d.ts +45 -0
- package/dist/recipes/scheduler.js +110 -0
- package/dist/recipes/scheduler.js.map +1 -0
- package/dist/recipes/schema.d.ts +71 -0
- package/dist/recipes/schema.js +11 -0
- package/dist/recipes/schema.js.map +1 -0
- package/dist/recipesHttp.d.ts +63 -0
- package/dist/recipesHttp.js +183 -0
- package/dist/recipesHttp.js.map +1 -0
- package/dist/resources.d.ts +33 -0
- package/dist/resources.js +266 -0
- package/dist/resources.js.map +1 -0
- package/dist/riskTier.d.ts +40 -0
- package/dist/riskTier.js +142 -0
- package/dist/riskTier.js.map +1 -0
- package/dist/runLog.d.ts +90 -0
- package/dist/runLog.js +143 -0
- package/dist/runLog.js.map +1 -0
- package/dist/server.d.ts +160 -0
- package/dist/server.js +1244 -0
- package/dist/server.js.map +1 -0
- package/dist/sessionCheckpoint.d.ts +37 -0
- package/dist/sessionCheckpoint.js +123 -0
- package/dist/sessionCheckpoint.js.map +1 -0
- package/dist/streamableHttp.d.ts +86 -0
- package/dist/streamableHttp.js +702 -0
- package/dist/streamableHttp.js.map +1 -0
- package/dist/telemetry.d.ts +18 -0
- package/dist/telemetry.js +95 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tools/activityLog.d.ts +140 -0
- package/dist/tools/activityLog.js +204 -0
- package/dist/tools/activityLog.js.map +1 -0
- package/dist/tools/auditDependencies.d.ts +67 -0
- package/dist/tools/auditDependencies.js +298 -0
- package/dist/tools/auditDependencies.js.map +1 -0
- package/dist/tools/batchLsp.d.ts +262 -0
- package/dist/tools/batchLsp.js +328 -0
- package/dist/tools/batchLsp.js.map +1 -0
- package/dist/tools/blame-utils.d.ts +30 -0
- package/dist/tools/blame-utils.js +60 -0
- package/dist/tools/blame-utils.js.map +1 -0
- package/dist/tools/bridgeDoctor.d.ts +78 -0
- package/dist/tools/bridgeDoctor.js +542 -0
- package/dist/tools/bridgeDoctor.js.map +1 -0
- package/dist/tools/bridgeStatus.d.ts +122 -0
- package/dist/tools/bridgeStatus.js +250 -0
- package/dist/tools/bridgeStatus.js.map +1 -0
- package/dist/tools/cancelClaudeTask.d.ts +48 -0
- package/dist/tools/cancelClaudeTask.js +56 -0
- package/dist/tools/cancelClaudeTask.js.map +1 -0
- package/dist/tools/checkDocumentDirty.d.ts +56 -0
- package/dist/tools/checkDocumentDirty.js +74 -0
- package/dist/tools/checkDocumentDirty.js.map +1 -0
- package/dist/tools/clipboard.d.ts +80 -0
- package/dist/tools/clipboard.js +211 -0
- package/dist/tools/clipboard.js.map +1 -0
- package/dist/tools/closeTabs.d.ts +84 -0
- package/dist/tools/closeTabs.js +97 -0
- package/dist/tools/closeTabs.js.map +1 -0
- package/dist/tools/codeLens.d.ts +50 -0
- package/dist/tools/codeLens.js +47 -0
- package/dist/tools/codeLens.js.map +1 -0
- package/dist/tools/contextBundle.d.ts +75 -0
- package/dist/tools/contextBundle.js +218 -0
- package/dist/tools/contextBundle.js.map +1 -0
- package/dist/tools/createIssueFromAIComment.d.ts +75 -0
- package/dist/tools/createIssueFromAIComment.js +119 -0
- package/dist/tools/createIssueFromAIComment.js.map +1 -0
- package/dist/tools/ctxGetTaskContext.d.ts +103 -0
- package/dist/tools/ctxGetTaskContext.js +274 -0
- package/dist/tools/ctxGetTaskContext.js.map +1 -0
- package/dist/tools/ctxQueryTraces.d.ts +142 -0
- package/dist/tools/ctxQueryTraces.js +194 -0
- package/dist/tools/ctxQueryTraces.js.map +1 -0
- package/dist/tools/ctxSaveTrace.d.ts +87 -0
- package/dist/tools/ctxSaveTrace.js +94 -0
- package/dist/tools/ctxSaveTrace.js.map +1 -0
- package/dist/tools/debug.d.ts +206 -0
- package/dist/tools/debug.js +234 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/decorations.d.ts +130 -0
- package/dist/tools/decorations.js +160 -0
- package/dist/tools/decorations.js.map +1 -0
- package/dist/tools/detectUnusedCode.d.ts +78 -0
- package/dist/tools/detectUnusedCode.js +173 -0
- package/dist/tools/detectUnusedCode.js.map +1 -0
- package/dist/tools/documentLinks.d.ts +62 -0
- package/dist/tools/documentLinks.js +55 -0
- package/dist/tools/documentLinks.js.map +1 -0
- package/dist/tools/editText.d.ts +108 -0
- package/dist/tools/editText.js +318 -0
- package/dist/tools/editText.js.map +1 -0
- package/dist/tools/enrichCommit.d.ts +89 -0
- package/dist/tools/enrichCommit.js +201 -0
- package/dist/tools/enrichCommit.js.map +1 -0
- package/dist/tools/enrichStackTrace.d.ts +121 -0
- package/dist/tools/enrichStackTrace.js +194 -0
- package/dist/tools/enrichStackTrace.js.map +1 -0
- package/dist/tools/explainDiagnostic.d.ts +137 -0
- package/dist/tools/explainDiagnostic.js +230 -0
- package/dist/tools/explainDiagnostic.js.map +1 -0
- package/dist/tools/explainSymbol.d.ts +119 -0
- package/dist/tools/explainSymbol.js +177 -0
- package/dist/tools/explainSymbol.js.map +1 -0
- package/dist/tools/fileOperations.d.ts +186 -0
- package/dist/tools/fileOperations.js +330 -0
- package/dist/tools/fileOperations.js.map +1 -0
- package/dist/tools/fileWatcher.d.ts +107 -0
- package/dist/tools/fileWatcher.js +121 -0
- package/dist/tools/fileWatcher.js.map +1 -0
- package/dist/tools/findFiles.d.ts +65 -0
- package/dist/tools/findFiles.js +142 -0
- package/dist/tools/findFiles.js.map +1 -0
- package/dist/tools/findRelatedTests.d.ts +83 -0
- package/dist/tools/findRelatedTests.js +196 -0
- package/dist/tools/findRelatedTests.js.map +1 -0
- package/dist/tools/fixAllLintErrors.d.ts +66 -0
- package/dist/tools/fixAllLintErrors.js +128 -0
- package/dist/tools/fixAllLintErrors.js.map +1 -0
- package/dist/tools/foldingRanges.d.ts +50 -0
- package/dist/tools/foldingRanges.js +51 -0
- package/dist/tools/foldingRanges.js.map +1 -0
- package/dist/tools/formatAndSave.d.ts +57 -0
- package/dist/tools/formatAndSave.js +87 -0
- package/dist/tools/formatAndSave.js.map +1 -0
- package/dist/tools/formatDocument.d.ts +61 -0
- package/dist/tools/formatDocument.js +144 -0
- package/dist/tools/formatDocument.js.map +1 -0
- package/dist/tools/generateAPIDocumentation.d.ts +62 -0
- package/dist/tools/generateAPIDocumentation.js +249 -0
- package/dist/tools/generateAPIDocumentation.js.map +1 -0
- package/dist/tools/generateTests.d.ts +75 -0
- package/dist/tools/generateTests.js +226 -0
- package/dist/tools/generateTests.js.map +1 -0
- package/dist/tools/getAIComments.d.ts +79 -0
- package/dist/tools/getAIComments.js +93 -0
- package/dist/tools/getAIComments.js.map +1 -0
- package/dist/tools/getAnalyticsReport.d.ts +102 -0
- package/dist/tools/getAnalyticsReport.js +137 -0
- package/dist/tools/getAnalyticsReport.js.map +1 -0
- package/dist/tools/getArchitectureContext.d.ts +85 -0
- package/dist/tools/getArchitectureContext.js +135 -0
- package/dist/tools/getArchitectureContext.js.map +1 -0
- package/dist/tools/getBufferContent.d.ts +80 -0
- package/dist/tools/getBufferContent.js +207 -0
- package/dist/tools/getBufferContent.js.map +1 -0
- package/dist/tools/getChangeImpact.d.ts +76 -0
- package/dist/tools/getChangeImpact.js +184 -0
- package/dist/tools/getChangeImpact.js.map +1 -0
- package/dist/tools/getClaudeTaskStatus.d.ts +87 -0
- package/dist/tools/getClaudeTaskStatus.js +89 -0
- package/dist/tools/getClaudeTaskStatus.js.map +1 -0
- package/dist/tools/getCodeCoverage.d.ts +86 -0
- package/dist/tools/getCodeCoverage.js +237 -0
- package/dist/tools/getCodeCoverage.js.map +1 -0
- package/dist/tools/getCommitsForIssue.d.ts +98 -0
- package/dist/tools/getCommitsForIssue.js +106 -0
- package/dist/tools/getCommitsForIssue.js.map +1 -0
- package/dist/tools/getCurrentSelection.d.ts +123 -0
- package/dist/tools/getCurrentSelection.js +113 -0
- package/dist/tools/getCurrentSelection.js.map +1 -0
- package/dist/tools/getDebugState.d.ts +140 -0
- package/dist/tools/getDebugState.js +109 -0
- package/dist/tools/getDebugState.js.map +1 -0
- package/dist/tools/getDependencyTree.d.ts +59 -0
- package/dist/tools/getDependencyTree.js +207 -0
- package/dist/tools/getDependencyTree.js.map +1 -0
- package/dist/tools/getDiagnostics.d.ts +108 -0
- package/dist/tools/getDiagnostics.js +371 -0
- package/dist/tools/getDiagnostics.js.map +1 -0
- package/dist/tools/getDiffFromHandoff.d.ts +89 -0
- package/dist/tools/getDiffFromHandoff.js +163 -0
- package/dist/tools/getDiffFromHandoff.js.map +1 -0
- package/dist/tools/getDocumentSymbols.d.ts +74 -0
- package/dist/tools/getDocumentSymbols.js +177 -0
- package/dist/tools/getDocumentSymbols.js.map +1 -0
- package/dist/tools/getFileTree.d.ts +66 -0
- package/dist/tools/getFileTree.js +131 -0
- package/dist/tools/getFileTree.js.map +1 -0
- package/dist/tools/getGitDiff.d.ts +50 -0
- package/dist/tools/getGitDiff.js +73 -0
- package/dist/tools/getGitDiff.js.map +1 -0
- package/dist/tools/getGitHotspots.d.ts +88 -0
- package/dist/tools/getGitHotspots.js +145 -0
- package/dist/tools/getGitHotspots.js.map +1 -0
- package/dist/tools/getGitLog.d.ts +62 -0
- package/dist/tools/getGitLog.js +87 -0
- package/dist/tools/getGitLog.js.map +1 -0
- package/dist/tools/getGitStatus.d.ts +72 -0
- package/dist/tools/getGitStatus.js +126 -0
- package/dist/tools/getGitStatus.js.map +1 -0
- package/dist/tools/getImportTree.d.ts +73 -0
- package/dist/tools/getImportTree.js +223 -0
- package/dist/tools/getImportTree.js.map +1 -0
- package/dist/tools/getImportedSignatures.d.ts +62 -0
- package/dist/tools/getImportedSignatures.js +255 -0
- package/dist/tools/getImportedSignatures.js.map +1 -0
- package/dist/tools/getOpenEditors.d.ts +62 -0
- package/dist/tools/getOpenEditors.js +126 -0
- package/dist/tools/getOpenEditors.js.map +1 -0
- package/dist/tools/getPRTemplate.d.ts +68 -0
- package/dist/tools/getPRTemplate.js +187 -0
- package/dist/tools/getPRTemplate.js.map +1 -0
- package/dist/tools/getProjectContext.d.ts +114 -0
- package/dist/tools/getProjectContext.js +344 -0
- package/dist/tools/getProjectContext.js.map +1 -0
- package/dist/tools/getProjectInfo.d.ts +51 -0
- package/dist/tools/getProjectInfo.js +325 -0
- package/dist/tools/getProjectInfo.js.map +1 -0
- package/dist/tools/getSecurityAdvisories.d.ts +105 -0
- package/dist/tools/getSecurityAdvisories.js +472 -0
- package/dist/tools/getSecurityAdvisories.js.map +1 -0
- package/dist/tools/getSessionUsage.d.ts +58 -0
- package/dist/tools/getSessionUsage.js +57 -0
- package/dist/tools/getSessionUsage.js.map +1 -0
- package/dist/tools/getSymbolHistory.d.ts +157 -0
- package/dist/tools/getSymbolHistory.js +256 -0
- package/dist/tools/getSymbolHistory.js.map +1 -0
- package/dist/tools/getToolCapabilities.d.ts +69 -0
- package/dist/tools/getToolCapabilities.js +298 -0
- package/dist/tools/getToolCapabilities.js.map +1 -0
- package/dist/tools/getTypeSignature.d.ts +70 -0
- package/dist/tools/getTypeSignature.js +132 -0
- package/dist/tools/getTypeSignature.js.map +1 -0
- package/dist/tools/getWorkspaceFolders.d.ts +58 -0
- package/dist/tools/getWorkspaceFolders.js +69 -0
- package/dist/tools/getWorkspaceFolders.js.map +1 -0
- package/dist/tools/getWorkspaceSettings.d.ts +44 -0
- package/dist/tools/getWorkspaceSettings.js +70 -0
- package/dist/tools/getWorkspaceSettings.js.map +1 -0
- package/dist/tools/git-utils.d.ts +16 -0
- package/dist/tools/git-utils.js +46 -0
- package/dist/tools/git-utils.js.map +1 -0
- package/dist/tools/gitHistory.d.ts +110 -0
- package/dist/tools/gitHistory.js +167 -0
- package/dist/tools/gitHistory.js.map +1 -0
- package/dist/tools/gitWrite.d.ts +612 -0
- package/dist/tools/gitWrite.js +983 -0
- package/dist/tools/gitWrite.js.map +1 -0
- package/dist/tools/github/actions.d.ts +152 -0
- package/dist/tools/github/actions.js +195 -0
- package/dist/tools/github/actions.js.map +1 -0
- package/dist/tools/github/index.d.ts +3 -0
- package/dist/tools/github/index.js +4 -0
- package/dist/tools/github/index.js.map +1 -0
- package/dist/tools/github/issues.d.ts +281 -0
- package/dist/tools/github/issues.js +340 -0
- package/dist/tools/github/issues.js.map +1 -0
- package/dist/tools/github/pr.d.ts +433 -0
- package/dist/tools/github/pr.js +588 -0
- package/dist/tools/github/pr.js.map +1 -0
- package/dist/tools/github/shared.d.ts +4 -0
- package/dist/tools/github/shared.js +12 -0
- package/dist/tools/github/shared.js.map +1 -0
- package/dist/tools/handoffNote.d.ts +106 -0
- package/dist/tools/handoffNote.js +232 -0
- package/dist/tools/handoffNote.js.map +1 -0
- package/dist/tools/headless/lspClient.d.ts +26 -0
- package/dist/tools/headless/lspClient.js +221 -0
- package/dist/tools/headless/lspClient.js.map +1 -0
- package/dist/tools/headless/lspFallback.d.ts +28 -0
- package/dist/tools/headless/lspFallback.js +122 -0
- package/dist/tools/headless/lspFallback.js.map +1 -0
- package/dist/tools/hoverAtCursor.d.ts +54 -0
- package/dist/tools/hoverAtCursor.js +68 -0
- package/dist/tools/hoverAtCursor.js.map +1 -0
- package/dist/tools/httpClient.d.ts +141 -0
- package/dist/tools/httpClient.js +486 -0
- package/dist/tools/httpClient.js.map +1 -0
- package/dist/tools/index.d.ts +49 -0
- package/dist/tools/index.js +672 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/inlayHints.d.ts +81 -0
- package/dist/tools/inlayHints.js +76 -0
- package/dist/tools/inlayHints.js.map +1 -0
- package/dist/tools/issueRefs.d.ts +14 -0
- package/dist/tools/issueRefs.js +27 -0
- package/dist/tools/issueRefs.js.map +1 -0
- package/dist/tools/jumpToFirstError.d.ts +63 -0
- package/dist/tools/jumpToFirstError.js +124 -0
- package/dist/tools/jumpToFirstError.js.map +1 -0
- package/dist/tools/launchQuickTask.d.ts +76 -0
- package/dist/tools/launchQuickTask.js +170 -0
- package/dist/tools/launchQuickTask.js.map +1 -0
- package/dist/tools/linters/biome.d.ts +2 -0
- package/dist/tools/linters/biome.js +44 -0
- package/dist/tools/linters/biome.js.map +1 -0
- package/dist/tools/linters/cargo.d.ts +2 -0
- package/dist/tools/linters/cargo.js +45 -0
- package/dist/tools/linters/cargo.js.map +1 -0
- package/dist/tools/linters/eslint.d.ts +2 -0
- package/dist/tools/linters/eslint.js +59 -0
- package/dist/tools/linters/eslint.js.map +1 -0
- package/dist/tools/linters/govet.d.ts +2 -0
- package/dist/tools/linters/govet.js +37 -0
- package/dist/tools/linters/govet.js.map +1 -0
- package/dist/tools/linters/pyright.d.ts +2 -0
- package/dist/tools/linters/pyright.js +34 -0
- package/dist/tools/linters/pyright.js.map +1 -0
- package/dist/tools/linters/ruff.d.ts +2 -0
- package/dist/tools/linters/ruff.js +30 -0
- package/dist/tools/linters/ruff.js.map +1 -0
- package/dist/tools/linters/types.d.ts +16 -0
- package/dist/tools/linters/types.js +2 -0
- package/dist/tools/linters/types.js.map +1 -0
- package/dist/tools/linters/typescript.d.ts +2 -0
- package/dist/tools/linters/typescript.js +38 -0
- package/dist/tools/linters/typescript.js.map +1 -0
- package/dist/tools/listClaudeTasks.d.ts +84 -0
- package/dist/tools/listClaudeTasks.js +88 -0
- package/dist/tools/listClaudeTasks.js.map +1 -0
- package/dist/tools/listTerminals.d.ts +55 -0
- package/dist/tools/listTerminals.js +78 -0
- package/dist/tools/listTerminals.js.map +1 -0
- package/dist/tools/lsp.d.ts +1086 -0
- package/dist/tools/lsp.js +1339 -0
- package/dist/tools/lsp.js.map +1 -0
- package/dist/tools/navigateToSymbolByName.d.ts +56 -0
- package/dist/tools/navigateToSymbolByName.js +170 -0
- package/dist/tools/navigateToSymbolByName.js.map +1 -0
- package/dist/tools/openDiff.d.ts +66 -0
- package/dist/tools/openDiff.js +126 -0
- package/dist/tools/openDiff.js.map +1 -0
- package/dist/tools/openFile.d.ts +69 -0
- package/dist/tools/openFile.js +129 -0
- package/dist/tools/openFile.js.map +1 -0
- package/dist/tools/openInBrowser.d.ts +55 -0
- package/dist/tools/openInBrowser.js +129 -0
- package/dist/tools/openInBrowser.js.map +1 -0
- package/dist/tools/organizeImports.d.ts +56 -0
- package/dist/tools/organizeImports.js +115 -0
- package/dist/tools/organizeImports.js.map +1 -0
- package/dist/tools/performanceReport.d.ts +133 -0
- package/dist/tools/performanceReport.js +218 -0
- package/dist/tools/performanceReport.js.map +1 -0
- package/dist/tools/planPersistence.d.ts +306 -0
- package/dist/tools/planPersistence.js +485 -0
- package/dist/tools/planPersistence.js.map +1 -0
- package/dist/tools/previewEdit.d.ts +107 -0
- package/dist/tools/previewEdit.js +270 -0
- package/dist/tools/previewEdit.js.map +1 -0
- package/dist/tools/recentTracesDigest.d.ts +35 -0
- package/dist/tools/recentTracesDigest.js +98 -0
- package/dist/tools/recentTracesDigest.js.map +1 -0
- package/dist/tools/refactorAnalyze.d.ts +78 -0
- package/dist/tools/refactorAnalyze.js +141 -0
- package/dist/tools/refactorAnalyze.js.map +1 -0
- package/dist/tools/refactorExtractFunction.d.ts +52 -0
- package/dist/tools/refactorExtractFunction.js +121 -0
- package/dist/tools/refactorExtractFunction.js.map +1 -0
- package/dist/tools/refactorPreview.d.ts +75 -0
- package/dist/tools/refactorPreview.js +93 -0
- package/dist/tools/refactorPreview.js.map +1 -0
- package/dist/tools/replaceBlock.d.ts +62 -0
- package/dist/tools/replaceBlock.js +125 -0
- package/dist/tools/replaceBlock.js.map +1 -0
- package/dist/tools/resumeClaudeTask.d.ts +75 -0
- package/dist/tools/resumeClaudeTask.js +149 -0
- package/dist/tools/resumeClaudeTask.js.map +1 -0
- package/dist/tools/runClaudeTask.d.ts +97 -0
- package/dist/tools/runClaudeTask.js +224 -0
- package/dist/tools/runClaudeTask.js.map +1 -0
- package/dist/tools/runCommand.d.ts +82 -0
- package/dist/tools/runCommand.js +101 -0
- package/dist/tools/runCommand.js.map +1 -0
- package/dist/tools/runTests.d.ts +146 -0
- package/dist/tools/runTests.js +315 -0
- package/dist/tools/runTests.js.map +1 -0
- package/dist/tools/saveDocument.d.ts +50 -0
- package/dist/tools/saveDocument.js +73 -0
- package/dist/tools/saveDocument.js.map +1 -0
- package/dist/tools/screenshot.d.ts +23 -0
- package/dist/tools/screenshot.js +43 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/screenshotAndAnnotate.d.ts +103 -0
- package/dist/tools/screenshotAndAnnotate.js +192 -0
- package/dist/tools/screenshotAndAnnotate.js.map +1 -0
- package/dist/tools/searchAndReplace.d.ts +108 -0
- package/dist/tools/searchAndReplace.js +281 -0
- package/dist/tools/searchAndReplace.js.map +1 -0
- package/dist/tools/searchTools.d.ts +61 -0
- package/dist/tools/searchTools.js +85 -0
- package/dist/tools/searchTools.js.map +1 -0
- package/dist/tools/searchWorkspace.d.ts +99 -0
- package/dist/tools/searchWorkspace.js +189 -0
- package/dist/tools/searchWorkspace.js.map +1 -0
- package/dist/tools/selectionRanges.d.ts +58 -0
- package/dist/tools/selectionRanges.js +61 -0
- package/dist/tools/selectionRanges.js.map +1 -0
- package/dist/tools/semanticTokens.d.ts +87 -0
- package/dist/tools/semanticTokens.js +86 -0
- package/dist/tools/semanticTokens.js.map +1 -0
- package/dist/tools/setActiveWorkspaceFolder.d.ts +41 -0
- package/dist/tools/setActiveWorkspaceFolder.js +38 -0
- package/dist/tools/setActiveWorkspaceFolder.js.map +1 -0
- package/dist/tools/signatureHelp.d.ts +86 -0
- package/dist/tools/signatureHelp.js +79 -0
- package/dist/tools/signatureHelp.js.map +1 -0
- package/dist/tools/spawnWorkspace.d.ts +103 -0
- package/dist/tools/spawnWorkspace.js +268 -0
- package/dist/tools/spawnWorkspace.js.map +1 -0
- package/dist/tools/stackTraceParser.d.ts +43 -0
- package/dist/tools/stackTraceParser.js +139 -0
- package/dist/tools/stackTraceParser.js.map +1 -0
- package/dist/tools/terminal.d.ts +352 -0
- package/dist/tools/terminal.js +670 -0
- package/dist/tools/terminal.js.map +1 -0
- package/dist/tools/testRunners/cargoTest.d.ts +2 -0
- package/dist/tools/testRunners/cargoTest.js +129 -0
- package/dist/tools/testRunners/cargoTest.js.map +1 -0
- package/dist/tools/testRunners/goTest.d.ts +2 -0
- package/dist/tools/testRunners/goTest.js +108 -0
- package/dist/tools/testRunners/goTest.js.map +1 -0
- package/dist/tools/testRunners/pytest.d.ts +2 -0
- package/dist/tools/testRunners/pytest.js +135 -0
- package/dist/tools/testRunners/pytest.js.map +1 -0
- package/dist/tools/testRunners/types.d.ts +18 -0
- package/dist/tools/testRunners/types.js +2 -0
- package/dist/tools/testRunners/types.js.map +1 -0
- package/dist/tools/testRunners/vitestJest.d.ts +3 -0
- package/dist/tools/testRunners/vitestJest.js +215 -0
- package/dist/tools/testRunners/vitestJest.js.map +1 -0
- package/dist/tools/testTraceToSource.d.ts +80 -0
- package/dist/tools/testTraceToSource.js +206 -0
- package/dist/tools/testTraceToSource.js.map +1 -0
- package/dist/tools/transaction.d.ts +243 -0
- package/dist/tools/transaction.js +309 -0
- package/dist/tools/transaction.js.map +1 -0
- package/dist/tools/typeHierarchy.d.ts +77 -0
- package/dist/tools/typeHierarchy.js +86 -0
- package/dist/tools/typeHierarchy.js.map +1 -0
- package/dist/tools/utils.d.ts +124 -0
- package/dist/tools/utils.js +566 -0
- package/dist/tools/utils.js.map +1 -0
- package/dist/tools/vscodeCommands.d.ts +90 -0
- package/dist/tools/vscodeCommands.js +112 -0
- package/dist/tools/vscodeCommands.js.map +1 -0
- package/dist/tools/vscodeTasks.d.ts +102 -0
- package/dist/tools/vscodeTasks.js +110 -0
- package/dist/tools/vscodeTasks.js.map +1 -0
- package/dist/tools/watchDiagnostics.d.ts +64 -0
- package/dist/tools/watchDiagnostics.js +270 -0
- package/dist/tools/watchDiagnostics.js.map +1 -0
- package/dist/tools/workspaceSettings.d.ts +57 -0
- package/dist/tools/workspaceSettings.js +80 -0
- package/dist/tools/workspaceSettings.js.map +1 -0
- package/dist/transport.d.ts +207 -0
- package/dist/transport.js +1272 -0
- package/dist/transport.js.map +1 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +31 -0
- package/dist/version.js.map +1 -0
- package/dist/wsUtils.d.ts +8 -0
- package/dist/wsUtils.js +54 -0
- package/dist/wsUtils.js.map +1 -0
- package/package.json +118 -0
- package/scripts/gen-claude-desktop-config.sh +124 -0
- package/scripts/gen-mcp-config.sh +390 -0
- package/scripts/install-extension.sh +106 -0
- package/scripts/mcp-stdio-shim.cjs +482 -0
- package/scripts/postinstall.mjs +68 -0
- package/scripts/start-all.sh +502 -0
- package/scripts/start-orchestrator.sh +186 -0
- package/scripts/start-remote.sh +126 -0
- package/scripts/start-vps.sh +116 -0
- package/templates/CLAUDE.bridge.md +125 -0
- package/templates/automation-policies/security-first.json +46 -0
- package/templates/automation-policies/strict-lint.json +41 -0
- package/templates/automation-policies/test-driven.json +54 -0
- package/templates/automation-policy.example.json +105 -0
- package/templates/bridge-tools.md +111 -0
- package/templates/dispatch-context.md +33 -0
- package/templates/managed-agent/code-review-agent.md +50 -0
- package/templates/managed-agent/managed-agent-mcp.json +102 -0
- package/templates/recipes/ambient-journal.yaml +11 -0
- package/templates/recipes/daily-status.yaml +21 -0
- package/templates/recipes/lint-on-save.yaml +13 -0
- package/templates/recipes/stale-branches.yaml +18 -0
- package/templates/recipes/watch-failing-tests.yaml +15 -0
- package/templates/scheduled-tasks/dependency-audit/SKILL.md +77 -0
- package/templates/scheduled-tasks/health-check/SKILL.md +73 -0
- package/templates/scheduled-tasks/nightly-review/SKILL.md +69 -0
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Authorization Server for claude-ide-bridge.
|
|
3
|
+
*
|
|
4
|
+
* Implements the MCP OAuth 2.0 profile required for authenticated remote servers:
|
|
5
|
+
* - RFC 8414 Authorization Server Metadata (/.well-known/oauth-authorization-server)
|
|
6
|
+
* - RFC 6749 Authorization Code Grant with PKCE (S256, RFC 7636)
|
|
7
|
+
* - RFC 7009 Token Revocation (/oauth/revoke)
|
|
8
|
+
*
|
|
9
|
+
* Design
|
|
10
|
+
* All state is in-memory. The bridge's static bearer token is the resource owner
|
|
11
|
+
* credential: only someone who knows it can open an OAuth flow via the approval page.
|
|
12
|
+
* Issued access tokens are opaque base64url strings stored in a TTL map.
|
|
13
|
+
* resolveBearerToken() is called by server.ts to admit OAuth-issued tokens alongside
|
|
14
|
+
* the static bridge token (backward compat).
|
|
15
|
+
* Refresh tokens are not issued.
|
|
16
|
+
*
|
|
17
|
+
* Security
|
|
18
|
+
* PKCE S256 mandatory. Auth codes single-use, 5 min TTL. Access tokens 24 h TTL.
|
|
19
|
+
* All string comparisons via crypto.timingSafeEqual. HTML output attribute-escaped.
|
|
20
|
+
*/
|
|
21
|
+
import crypto from "node:crypto";
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { URL } from "node:url";
|
|
25
|
+
import { timingSafeStringEqual } from "./crypto.js";
|
|
26
|
+
// ── CIMD SSRF guard ───────────────────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Blocks private/loopback hostnames for CIMD fetches.
|
|
29
|
+
* CIMD URLs must be public HTTPS — rejecting private addresses prevents
|
|
30
|
+
* SSRF via a crafted client_id URL.
|
|
31
|
+
*/
|
|
32
|
+
function isPrivateCimdHost(hostname) {
|
|
33
|
+
const h = hostname.toLowerCase().replace(/^\[|]$/g, ""); // strip IPv6 brackets
|
|
34
|
+
if (h === "localhost" ||
|
|
35
|
+
h.startsWith("127.") ||
|
|
36
|
+
h.startsWith("10.") ||
|
|
37
|
+
h.startsWith("192.168.") ||
|
|
38
|
+
h === "::1" ||
|
|
39
|
+
h.startsWith("fc") ||
|
|
40
|
+
h.startsWith("fd") ||
|
|
41
|
+
h.startsWith("169.254."))
|
|
42
|
+
return true;
|
|
43
|
+
// 172.16.0.0/12
|
|
44
|
+
const m = /^172\.(\d+)\./.exec(h);
|
|
45
|
+
if (m && Number(m[1]) >= 16 && Number(m[1]) <= 31)
|
|
46
|
+
return true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
50
|
+
const CODE_TTL_MS = 5 * 60 * 1_000; // 5 min
|
|
51
|
+
const TOKEN_TTL_MS = 24 * 60 * 60 * 1_000; // 24 hours
|
|
52
|
+
const CLIENT_TTL_MS = 7 * 24 * 60 * 60 * 1_000; // 7 days — GC registered clients after a week
|
|
53
|
+
const DEFAULT_SCOPE = "mcp";
|
|
54
|
+
const SUPPORTED_SCOPES = ["mcp"];
|
|
55
|
+
// ── OAuthServerImpl ───────────────────────────────────────────────────────────
|
|
56
|
+
export class OAuthServerImpl {
|
|
57
|
+
bridgeToken;
|
|
58
|
+
issuerUrl;
|
|
59
|
+
authCodes = new Map();
|
|
60
|
+
accessTokens = new Map();
|
|
61
|
+
/** client_id → { redirectUris, issuedAt } (populated by handleRegister) */
|
|
62
|
+
registeredClients = new Map();
|
|
63
|
+
/** Per-IP registration rate limit: IP → { count, windowStart } */
|
|
64
|
+
registerIpCounts = new Map();
|
|
65
|
+
static REGISTER_IP_MAX = 10; // max registrations per IP per minute
|
|
66
|
+
static REGISTER_IP_WINDOW_MS = 60 * 1_000;
|
|
67
|
+
gcTimer;
|
|
68
|
+
/** CSRF nonces: flowId → { nonce, clientId, expiresAt } */
|
|
69
|
+
csrfNonces = new Map();
|
|
70
|
+
static CSRF_TTL_MS = 10 * 60 * 1_000; // 10 minutes
|
|
71
|
+
/**
|
|
72
|
+
* CIMD cache: client_id URL → { redirectUris, fetchedAt }
|
|
73
|
+
* Short TTL (5 min) — avoids re-fetching on every authorize request while
|
|
74
|
+
* staying fresh enough for clients that rotate their metadata.
|
|
75
|
+
*/
|
|
76
|
+
cimdCache = new Map();
|
|
77
|
+
static CIMD_CACHE_TTL_MS = 5 * 60 * 1_000; // 5 min
|
|
78
|
+
static CIMD_MAX_BYTES = 8_192; // 8 KB max for metadata doc
|
|
79
|
+
/** Path to the persisted token file; null when persistence is disabled. */
|
|
80
|
+
tokenStorePath;
|
|
81
|
+
tokenTtlMs;
|
|
82
|
+
/**
|
|
83
|
+
* Tokens loaded from disk on startup: SHA-256(token) → AccessToken.
|
|
84
|
+
* In-memory issued tokens use `accessTokens` (raw key). Both are checked
|
|
85
|
+
* in resolveBearerToken — disk tokens are promoted to accessTokens on first use.
|
|
86
|
+
*/
|
|
87
|
+
hashedTokens = new Map();
|
|
88
|
+
persistTimer = null;
|
|
89
|
+
constructor(bridgeToken, issuerUrl, opts) {
|
|
90
|
+
this.bridgeToken = bridgeToken;
|
|
91
|
+
this.issuerUrl = issuerUrl.replace(/\/$/, "");
|
|
92
|
+
this.tokenTtlMs = opts?.tokenTtlMs ?? TOKEN_TTL_MS;
|
|
93
|
+
this.tokenStorePath = opts?.configDir
|
|
94
|
+
? path.join(opts.configDir, "ide", "oauth-tokens.json")
|
|
95
|
+
: null;
|
|
96
|
+
this.gcTimer = setInterval(() => {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
for (const [k, v] of this.authCodes)
|
|
99
|
+
if (v.expiresAt < now)
|
|
100
|
+
this.authCodes.delete(k);
|
|
101
|
+
for (const [k, v] of this.accessTokens)
|
|
102
|
+
if (v.expiresAt < now)
|
|
103
|
+
this.accessTokens.delete(k);
|
|
104
|
+
for (const [k, v] of this.registeredClients)
|
|
105
|
+
if (now - v.issuedAt > CLIENT_TTL_MS)
|
|
106
|
+
this.registeredClients.delete(k);
|
|
107
|
+
for (const [k, v] of this.csrfNonces)
|
|
108
|
+
if (v.expiresAt < now)
|
|
109
|
+
this.csrfNonces.delete(k);
|
|
110
|
+
for (const [k, v] of this.registerIpCounts)
|
|
111
|
+
if (now - v.windowStart > OAuthServerImpl.REGISTER_IP_WINDOW_MS)
|
|
112
|
+
this.registerIpCounts.delete(k);
|
|
113
|
+
for (const [k, v] of this.cimdCache)
|
|
114
|
+
if (now - v.fetchedAt > OAuthServerImpl.CIMD_CACHE_TTL_MS)
|
|
115
|
+
this.cimdCache.delete(k);
|
|
116
|
+
}, 10 * 60 * 1_000);
|
|
117
|
+
this.gcTimer.unref();
|
|
118
|
+
this.loadTokens();
|
|
119
|
+
}
|
|
120
|
+
destroy() {
|
|
121
|
+
// Flush any pending debounced persist before shutdown
|
|
122
|
+
if (this.persistTimer !== null) {
|
|
123
|
+
clearTimeout(this.persistTimer);
|
|
124
|
+
this.persistTimer = null;
|
|
125
|
+
this.persistTokens();
|
|
126
|
+
}
|
|
127
|
+
clearInterval(this.gcTimer);
|
|
128
|
+
}
|
|
129
|
+
// ── RFC 8414 discovery ────────────────────────────────────────────────────
|
|
130
|
+
handleDiscovery(res) {
|
|
131
|
+
this.sendJson(res, 200, {
|
|
132
|
+
issuer: this.issuerUrl,
|
|
133
|
+
authorization_endpoint: `${this.issuerUrl}/oauth/authorize`,
|
|
134
|
+
token_endpoint: `${this.issuerUrl}/oauth/token`,
|
|
135
|
+
revocation_endpoint: `${this.issuerUrl}/oauth/revoke`,
|
|
136
|
+
registration_endpoint: `${this.issuerUrl}/oauth/register`,
|
|
137
|
+
response_types_supported: ["code"],
|
|
138
|
+
grant_types_supported: ["authorization_code"],
|
|
139
|
+
code_challenge_methods_supported: ["S256"],
|
|
140
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
141
|
+
scopes_supported: SUPPORTED_SCOPES,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// ── RFC 7591 Dynamic Client Registration ──────────────────────────────────
|
|
145
|
+
async handleRegister(req, res) {
|
|
146
|
+
if (req.method !== "POST") {
|
|
147
|
+
res.writeHead(405, { Allow: "POST" });
|
|
148
|
+
res.end("Method Not Allowed");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
let body = {};
|
|
152
|
+
try {
|
|
153
|
+
const raw = await new Promise((resolve, reject) => {
|
|
154
|
+
let data = "";
|
|
155
|
+
const onData = (c) => {
|
|
156
|
+
data += c.toString();
|
|
157
|
+
if (data.length > 8192) {
|
|
158
|
+
req.removeListener("data", onData);
|
|
159
|
+
req.removeListener("end", onEnd);
|
|
160
|
+
req.destroy();
|
|
161
|
+
reject(new Error("too large"));
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const onEnd = () => resolve(data);
|
|
165
|
+
req.on("data", onData);
|
|
166
|
+
req.on("end", onEnd);
|
|
167
|
+
req.on("error", reject);
|
|
168
|
+
});
|
|
169
|
+
body = raw ? JSON.parse(raw) : {};
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
this.sendJson(res, 400, { error: "invalid_client_metadata" });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const redirectUris = body.redirect_uris;
|
|
176
|
+
if (!Array.isArray(redirectUris) || redirectUris.length === 0) {
|
|
177
|
+
this.sendJson(res, 400, { error: "invalid_redirect_uri" });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Validate each redirect_uri is a well-formed absolute HTTPS (or localhost) URL
|
|
181
|
+
for (const uri of redirectUris) {
|
|
182
|
+
if (typeof uri !== "string") {
|
|
183
|
+
this.sendJson(res, 400, { error: "invalid_redirect_uri" });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const u = new URL(uri);
|
|
188
|
+
const isLocalhost = u.hostname === "localhost" || u.hostname === "127.0.0.1";
|
|
189
|
+
if (!isLocalhost && u.protocol !== "https:") {
|
|
190
|
+
this.sendJson(res, 400, { error: "invalid_redirect_uri" });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
this.sendJson(res, 400, { error: "invalid_redirect_uri" });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Validate scope if provided
|
|
200
|
+
if (body.scope !== undefined) {
|
|
201
|
+
const requestedScopes = String(body.scope).split(" ");
|
|
202
|
+
for (const s of requestedScopes) {
|
|
203
|
+
if (!SUPPORTED_SCOPES.includes(s)) {
|
|
204
|
+
this.sendJson(res, 400, {
|
|
205
|
+
error: "invalid_client_metadata",
|
|
206
|
+
error_description: `unsupported scope: ${s}`,
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Per-IP rate limit: max 10 registrations per minute per IP
|
|
213
|
+
const remoteIp = (req.socket?.remoteAddress ?? "unknown").slice(0, 64);
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
const ipEntry = this.registerIpCounts.get(remoteIp);
|
|
216
|
+
if (ipEntry &&
|
|
217
|
+
now - ipEntry.windowStart < OAuthServerImpl.REGISTER_IP_WINDOW_MS) {
|
|
218
|
+
ipEntry.count++;
|
|
219
|
+
if (ipEntry.count > OAuthServerImpl.REGISTER_IP_MAX) {
|
|
220
|
+
this.sendJson(res, 429, {
|
|
221
|
+
error: "too_many_requests",
|
|
222
|
+
error_description: "per-IP client registration limit reached",
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
this.registerIpCounts.set(remoteIp, { count: 1, windowStart: now });
|
|
229
|
+
}
|
|
230
|
+
// Cap registered clients to prevent memory exhaustion via pre-auth DoS.
|
|
231
|
+
// /oauth/register requires no bearer token, so any caller can POST freely.
|
|
232
|
+
if (this.registeredClients.size >= 500) {
|
|
233
|
+
this.sendJson(res, 429, {
|
|
234
|
+
error: "too_many_requests",
|
|
235
|
+
error_description: "client registration limit reached",
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Public clients only — no client secret issued
|
|
240
|
+
const clientId = this.randomToken(16);
|
|
241
|
+
this.registeredClients.set(clientId, {
|
|
242
|
+
redirectUris: redirectUris,
|
|
243
|
+
issuedAt: Date.now(),
|
|
244
|
+
});
|
|
245
|
+
this.sendJson(res, 201, {
|
|
246
|
+
client_id: clientId,
|
|
247
|
+
client_id_issued_at: Math.floor(Date.now() / 1000),
|
|
248
|
+
redirect_uris: redirectUris,
|
|
249
|
+
grant_types: ["authorization_code"],
|
|
250
|
+
response_types: ["code"],
|
|
251
|
+
token_endpoint_auth_method: "none",
|
|
252
|
+
...(body.client_name
|
|
253
|
+
? {
|
|
254
|
+
client_name: typeof body.client_name === "string"
|
|
255
|
+
? body.client_name.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 128)
|
|
256
|
+
: undefined,
|
|
257
|
+
}
|
|
258
|
+
: {}),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
// ── Authorization endpoint ────────────────────────────────────────────────
|
|
262
|
+
async handleAuthorize(req, res) {
|
|
263
|
+
const method = req.method ?? "GET";
|
|
264
|
+
if (method === "GET") {
|
|
265
|
+
await this.authorizeGet(req, res);
|
|
266
|
+
}
|
|
267
|
+
else if (method === "POST") {
|
|
268
|
+
await this.authorizePost(req, res);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
res.writeHead(405, { "Content-Type": "text/plain", Allow: "GET, POST" });
|
|
272
|
+
res.end("Method Not Allowed");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async authorizeGet(req, res) {
|
|
276
|
+
const url = new URL(req.url ?? "/", this.issuerUrl);
|
|
277
|
+
const { error, clientId, redirectUri, codeChallenge, scope, state } = await this.parseAuthorizeParams(url);
|
|
278
|
+
if (error) {
|
|
279
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
280
|
+
res.end(error);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Generate CSRF nonce keyed by a random flowId (not client_id) so concurrent
|
|
284
|
+
// authorization flows for the same client_id cannot overwrite each other's nonce.
|
|
285
|
+
const csrfNonce = crypto.randomBytes(16).toString("hex");
|
|
286
|
+
const flowId = crypto.randomBytes(8).toString("hex");
|
|
287
|
+
this.csrfNonces.set(flowId, {
|
|
288
|
+
nonce: csrfNonce,
|
|
289
|
+
clientId: clientId,
|
|
290
|
+
expiresAt: Date.now() + OAuthServerImpl.CSRF_TTL_MS,
|
|
291
|
+
});
|
|
292
|
+
res.writeHead(200, {
|
|
293
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
294
|
+
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'",
|
|
295
|
+
"X-Frame-Options": "DENY",
|
|
296
|
+
});
|
|
297
|
+
res.end(this.approvalPage({
|
|
298
|
+
clientId: clientId,
|
|
299
|
+
redirectUri: redirectUri,
|
|
300
|
+
codeChallenge: codeChallenge,
|
|
301
|
+
scope: scope ?? DEFAULT_SCOPE,
|
|
302
|
+
state: state ?? "",
|
|
303
|
+
csrfNonce,
|
|
304
|
+
flowId,
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
async authorizePost(req, res) {
|
|
308
|
+
let body;
|
|
309
|
+
try {
|
|
310
|
+
body = await this.readBody(req);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
314
|
+
res.end("request body too large");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const action = body.get("action");
|
|
318
|
+
const clientId = body.get("client_id") ?? "";
|
|
319
|
+
const redirectUri = body.get("redirect_uri") ?? "";
|
|
320
|
+
const codeChallenge = body.get("code_challenge") ?? "";
|
|
321
|
+
const scope = body.get("scope") ?? DEFAULT_SCOPE;
|
|
322
|
+
const state = body.get("state") ?? "";
|
|
323
|
+
const csrfNonce = body.get("csrf_nonce") ?? "";
|
|
324
|
+
const flowId = body.get("flow_id") ?? "";
|
|
325
|
+
if (!clientId || !redirectUri || !codeChallenge) {
|
|
326
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
327
|
+
res.end("missing parameters");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
// Verify CSRF nonce before any further processing.
|
|
331
|
+
// Look up by flowId (not clientId) to prevent concurrent-flow nonce collision attacks.
|
|
332
|
+
const storedCsrf = this.csrfNonces.get(flowId);
|
|
333
|
+
if (!storedCsrf ||
|
|
334
|
+
storedCsrf.expiresAt < Date.now() ||
|
|
335
|
+
storedCsrf.clientId !== clientId ||
|
|
336
|
+
!timingSafeStringEqual(csrfNonce, storedCsrf.nonce)) {
|
|
337
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
338
|
+
res.end("invalid or expired CSRF token");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Consume the nonce (one-time use)
|
|
342
|
+
this.csrfNonces.delete(flowId);
|
|
343
|
+
// Validate redirect_uri against registered URIs to prevent open redirect
|
|
344
|
+
const registered = this.registeredClients.get(clientId);
|
|
345
|
+
if (!registered?.redirectUris.includes(redirectUri)) {
|
|
346
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
347
|
+
res.end("invalid redirect_uri");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (action === "deny") {
|
|
351
|
+
const u = new URL(redirectUri);
|
|
352
|
+
u.searchParams.set("error", "access_denied");
|
|
353
|
+
if (state)
|
|
354
|
+
u.searchParams.set("state", state);
|
|
355
|
+
res.writeHead(302, { Location: u.toString() });
|
|
356
|
+
res.end();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (action !== "approve") {
|
|
360
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
361
|
+
res.end("invalid action");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// Verify bridge token on approve
|
|
365
|
+
const presentedToken = body.get("bridge_token") ?? "";
|
|
366
|
+
if (!timingSafeStringEqual(presentedToken, this.bridgeToken)) {
|
|
367
|
+
// Issue a fresh flowId + nonce for the retry so the form remains usable.
|
|
368
|
+
const retryCsrfNonce = crypto.randomBytes(16).toString("hex");
|
|
369
|
+
const retryFlowId = crypto.randomBytes(8).toString("hex");
|
|
370
|
+
this.csrfNonces.set(retryFlowId, {
|
|
371
|
+
nonce: retryCsrfNonce,
|
|
372
|
+
clientId,
|
|
373
|
+
expiresAt: Date.now() + OAuthServerImpl.CSRF_TTL_MS,
|
|
374
|
+
});
|
|
375
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
376
|
+
res.end(this.approvalPage({
|
|
377
|
+
clientId,
|
|
378
|
+
redirectUri,
|
|
379
|
+
codeChallenge,
|
|
380
|
+
scope,
|
|
381
|
+
state,
|
|
382
|
+
tokenError: true,
|
|
383
|
+
csrfNonce: retryCsrfNonce,
|
|
384
|
+
flowId: retryFlowId,
|
|
385
|
+
}));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const code = this.randomToken(32);
|
|
389
|
+
this.authCodes.set(code, {
|
|
390
|
+
clientId,
|
|
391
|
+
redirectUri,
|
|
392
|
+
codeChallenge,
|
|
393
|
+
scope,
|
|
394
|
+
expiresAt: Date.now() + CODE_TTL_MS,
|
|
395
|
+
used: false,
|
|
396
|
+
});
|
|
397
|
+
const dest = new URL(redirectUri);
|
|
398
|
+
dest.searchParams.set("code", code);
|
|
399
|
+
if (state)
|
|
400
|
+
dest.searchParams.set("state", state);
|
|
401
|
+
res.writeHead(302, { Location: dest.toString() });
|
|
402
|
+
res.end();
|
|
403
|
+
}
|
|
404
|
+
// ── Token endpoint ────────────────────────────────────────────────────────
|
|
405
|
+
async handleToken(req, res) {
|
|
406
|
+
let body;
|
|
407
|
+
try {
|
|
408
|
+
body = await this.readBody(req);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
this.sendError(res, 400, "invalid_request");
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (body.get("grant_type") !== "authorization_code") {
|
|
415
|
+
this.sendError(res, 400, "unsupported_grant_type");
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const code = body.get("code") ?? "";
|
|
419
|
+
const redirectUri = body.get("redirect_uri") ?? "";
|
|
420
|
+
const clientId = body.get("client_id") ?? "";
|
|
421
|
+
const verifier = body.get("code_verifier") ?? "";
|
|
422
|
+
if (!code || !redirectUri || !clientId || !verifier) {
|
|
423
|
+
this.sendError(res, 400, "invalid_request", "missing required parameters");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const record = this.authCodes.get(code);
|
|
427
|
+
if (!record) {
|
|
428
|
+
this.sendError(res, 400, "invalid_grant", "authorization code not found or expired");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (record.used) {
|
|
432
|
+
this.sendError(res, 400, "invalid_grant", "authorization code already used");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (record.expiresAt < Date.now()) {
|
|
436
|
+
this.authCodes.delete(code);
|
|
437
|
+
this.sendError(res, 400, "invalid_grant", "authorization code expired");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (!timingSafeStringEqual(record.clientId, clientId)) {
|
|
441
|
+
this.sendError(res, 400, "invalid_grant", "client_id mismatch");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (!timingSafeStringEqual(record.redirectUri, redirectUri)) {
|
|
445
|
+
this.sendError(res, 400, "invalid_grant", "redirect_uri mismatch");
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (!this.pkceVerify(verifier, record.codeChallenge)) {
|
|
449
|
+
this.sendError(res, 400, "invalid_grant", "code_verifier mismatch");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// RFC 6749 §4.1.2: delete the auth code immediately on use.
|
|
453
|
+
// A missing code naturally rejects replay attempts (the !record check above).
|
|
454
|
+
this.authCodes.delete(code);
|
|
455
|
+
const accessToken = this.randomToken(32);
|
|
456
|
+
this.accessTokens.set(accessToken, {
|
|
457
|
+
clientId,
|
|
458
|
+
scope: record.scope,
|
|
459
|
+
expiresAt: Date.now() + this.tokenTtlMs,
|
|
460
|
+
});
|
|
461
|
+
this.schedulePersist();
|
|
462
|
+
this.sendJson(res, 200, {
|
|
463
|
+
access_token: accessToken,
|
|
464
|
+
token_type: "Bearer",
|
|
465
|
+
expires_in: Math.floor(this.tokenTtlMs / 1_000),
|
|
466
|
+
scope: record.scope,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
// ── Revocation endpoint (RFC 7009) ────────────────────────────────────────
|
|
470
|
+
async handleRevoke(req, res) {
|
|
471
|
+
try {
|
|
472
|
+
const body = await this.readBody(req);
|
|
473
|
+
const token = body.get("token");
|
|
474
|
+
if (token) {
|
|
475
|
+
this.accessTokens.delete(token);
|
|
476
|
+
this.authCodes.delete(token);
|
|
477
|
+
// Also remove from hashed (disk-persisted) tokens
|
|
478
|
+
const hash = this.hashToken(token);
|
|
479
|
+
this.hashedTokens.delete(hash);
|
|
480
|
+
this.schedulePersist();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// RFC 7009: always 200
|
|
485
|
+
}
|
|
486
|
+
res.writeHead(200, {
|
|
487
|
+
"Content-Type": "application/json",
|
|
488
|
+
"Cache-Control": "no-store",
|
|
489
|
+
});
|
|
490
|
+
res.end("{}");
|
|
491
|
+
}
|
|
492
|
+
// ── Bearer resolution (called by server.ts) ───────────────────────────────
|
|
493
|
+
resolveBearerToken(token) {
|
|
494
|
+
const record = this.lookupToken(token);
|
|
495
|
+
if (!record)
|
|
496
|
+
return null;
|
|
497
|
+
return this.bridgeToken;
|
|
498
|
+
}
|
|
499
|
+
resolveBearerScope(token) {
|
|
500
|
+
const record = this.lookupToken(token);
|
|
501
|
+
if (!record)
|
|
502
|
+
return null;
|
|
503
|
+
return record.scope;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Lookup a bearer token in memory first, then fall back to disk-loaded hashed
|
|
507
|
+
* tokens. Promotes disk tokens to the in-memory map on first use for fast
|
|
508
|
+
* subsequent lookups.
|
|
509
|
+
*/
|
|
510
|
+
lookupToken(token) {
|
|
511
|
+
// 1. Check in-memory map (tokens issued this session)
|
|
512
|
+
const inMemory = this.accessTokens.get(token);
|
|
513
|
+
if (inMemory) {
|
|
514
|
+
if (inMemory.expiresAt < Date.now()) {
|
|
515
|
+
this.accessTokens.delete(token);
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
return inMemory;
|
|
519
|
+
}
|
|
520
|
+
// 2. Check disk-loaded hashed tokens (tokens issued before last restart)
|
|
521
|
+
const hash = this.hashToken(token);
|
|
522
|
+
const fromDisk = this.hashedTokens.get(hash);
|
|
523
|
+
if (fromDisk) {
|
|
524
|
+
if (fromDisk.expiresAt < Date.now()) {
|
|
525
|
+
this.hashedTokens.delete(hash);
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
// Promote to in-memory for fast subsequent lookups
|
|
529
|
+
this.accessTokens.set(token, fromDisk);
|
|
530
|
+
this.hashedTokens.delete(hash);
|
|
531
|
+
return fromDisk;
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
// ── Token persistence helpers ─────────────────────────────────────────────
|
|
536
|
+
hashToken(token) {
|
|
537
|
+
return crypto.createHash("sha256").update(token).digest("hex");
|
|
538
|
+
}
|
|
539
|
+
loadTokens() {
|
|
540
|
+
if (!this.tokenStorePath)
|
|
541
|
+
return;
|
|
542
|
+
try {
|
|
543
|
+
const raw = fs.readFileSync(this.tokenStorePath, "utf8");
|
|
544
|
+
const parsed = JSON.parse(raw);
|
|
545
|
+
if (parsed.version !== 1 || typeof parsed.tokens !== "object")
|
|
546
|
+
return;
|
|
547
|
+
const entries = Object.entries(parsed.tokens);
|
|
548
|
+
// Cap: refuse to load a file with suspiciously many entries to prevent DoS
|
|
549
|
+
if (entries.length > 10_000) {
|
|
550
|
+
console.warn("[claude-ide-bridge] oauth-tokens.json contains >10,000 entries — skipping load");
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const now = Date.now();
|
|
554
|
+
for (const [hash, entry] of entries) {
|
|
555
|
+
// Validate each field before trusting the persisted data
|
|
556
|
+
if (typeof entry.clientId !== "string" ||
|
|
557
|
+
typeof entry.scope !== "string" ||
|
|
558
|
+
typeof entry.expiresAt !== "number" ||
|
|
559
|
+
!Number.isFinite(entry.expiresAt) ||
|
|
560
|
+
entry.expiresAt <= 0 ||
|
|
561
|
+
!SUPPORTED_SCOPES.some((s) => entry.scope === s ||
|
|
562
|
+
entry.scope
|
|
563
|
+
.split(" ")
|
|
564
|
+
.every((tok) => SUPPORTED_SCOPES.includes(tok)))) {
|
|
565
|
+
continue; // skip invalid entries
|
|
566
|
+
}
|
|
567
|
+
if (entry.expiresAt > now) {
|
|
568
|
+
this.hashedTokens.set(hash, {
|
|
569
|
+
clientId: entry.clientId,
|
|
570
|
+
scope: entry.scope,
|
|
571
|
+
expiresAt: entry.expiresAt,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
// Missing or corrupt file — start fresh
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
persistTokens() {
|
|
581
|
+
if (!this.tokenStorePath)
|
|
582
|
+
return;
|
|
583
|
+
try {
|
|
584
|
+
const now = Date.now();
|
|
585
|
+
const tokens = {};
|
|
586
|
+
// Persist current in-memory tokens (keyed by hash).
|
|
587
|
+
// Invariant: a promoted disk token exists ONLY in `accessTokens` — it was
|
|
588
|
+
// deleted from `hashedTokens` at promotion time. Do not write it from both
|
|
589
|
+
// maps or revocation tracking breaks.
|
|
590
|
+
for (const [rawToken, record] of this.accessTokens) {
|
|
591
|
+
if (record.expiresAt > now) {
|
|
592
|
+
tokens[this.hashToken(rawToken)] = {
|
|
593
|
+
clientId: record.clientId,
|
|
594
|
+
scope: record.scope,
|
|
595
|
+
expiresAt: record.expiresAt,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// Persist still-valid disk tokens that have not yet been promoted.
|
|
600
|
+
// The `!(hash in tokens)` guard is a safety net — promoted tokens should
|
|
601
|
+
// already be absent from `hashedTokens`, but the check prevents any
|
|
602
|
+
// double-write if that invariant is ever violated.
|
|
603
|
+
for (const [hash, record] of this.hashedTokens) {
|
|
604
|
+
if (record.expiresAt > now && !(hash in tokens)) {
|
|
605
|
+
tokens[hash] = {
|
|
606
|
+
clientId: record.clientId,
|
|
607
|
+
scope: record.scope,
|
|
608
|
+
expiresAt: record.expiresAt,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const data = JSON.stringify({ version: 1, tokens }, null, 2);
|
|
613
|
+
const tmpPath = `${this.tokenStorePath}.tmp`;
|
|
614
|
+
const dir = path.dirname(this.tokenStorePath);
|
|
615
|
+
if (!fs.existsSync(dir))
|
|
616
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
617
|
+
fs.writeFileSync(tmpPath, data, { mode: 0o600 });
|
|
618
|
+
fs.renameSync(tmpPath, this.tokenStorePath);
|
|
619
|
+
fs.chmodSync(this.tokenStorePath, 0o600);
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
// Best-effort — never block operation
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
schedulePersist() {
|
|
626
|
+
if (!this.tokenStorePath)
|
|
627
|
+
return;
|
|
628
|
+
if (this.persistTimer !== null)
|
|
629
|
+
clearTimeout(this.persistTimer);
|
|
630
|
+
this.persistTimer = setTimeout(() => {
|
|
631
|
+
this.persistTimer = null;
|
|
632
|
+
this.persistTokens();
|
|
633
|
+
}, 500).unref(); // unref so a pending flush doesn't prevent process exit
|
|
634
|
+
}
|
|
635
|
+
// ── Private helpers ───────────────────────────────────────────────────────
|
|
636
|
+
randomToken(bytes) {
|
|
637
|
+
return crypto.randomBytes(bytes).toString("base64url");
|
|
638
|
+
}
|
|
639
|
+
pkceVerify(verifier, challenge) {
|
|
640
|
+
const hash = crypto
|
|
641
|
+
.createHash("sha256")
|
|
642
|
+
.update(verifier)
|
|
643
|
+
.digest("base64url");
|
|
644
|
+
return timingSafeStringEqual(hash, challenge);
|
|
645
|
+
}
|
|
646
|
+
readBody(req) {
|
|
647
|
+
return new Promise((resolve, reject) => {
|
|
648
|
+
let data = "";
|
|
649
|
+
const onData = (chunk) => {
|
|
650
|
+
data += chunk.toString();
|
|
651
|
+
if (data.length > 8_192) {
|
|
652
|
+
req.removeListener("data", onData);
|
|
653
|
+
req.removeListener("end", onEnd);
|
|
654
|
+
req.destroy();
|
|
655
|
+
reject(new Error("Request body too large"));
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
const onEnd = () => resolve(new URLSearchParams(data));
|
|
659
|
+
req.on("data", onData);
|
|
660
|
+
req.on("end", onEnd);
|
|
661
|
+
req.on("error", reject);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
sendJson(res, status, body) {
|
|
665
|
+
res.writeHead(status, {
|
|
666
|
+
"Content-Type": "application/json",
|
|
667
|
+
"Cache-Control": "no-store",
|
|
668
|
+
Pragma: "no-cache",
|
|
669
|
+
});
|
|
670
|
+
res.end(JSON.stringify(body));
|
|
671
|
+
}
|
|
672
|
+
sendError(res, status, error, description) {
|
|
673
|
+
this.sendJson(res, status, {
|
|
674
|
+
error,
|
|
675
|
+
...(description ? { error_description: description } : {}),
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Fetch and cache a Client ID Metadata Document (CIMD / SEP-991).
|
|
680
|
+
* Called when client_id is an HTTPS URL instead of an opaque registered ID.
|
|
681
|
+
* Returns the redirect_uris from the document, or null on any error.
|
|
682
|
+
*
|
|
683
|
+
* Security: only public HTTPS URLs are allowed (isPrivateCimdHost blocks
|
|
684
|
+
* RFC 1918 / loopback). Response size capped at CIMD_MAX_BYTES.
|
|
685
|
+
*/
|
|
686
|
+
async fetchCimd(clientIdUrl) {
|
|
687
|
+
const cached = this.cimdCache.get(clientIdUrl);
|
|
688
|
+
if (cached &&
|
|
689
|
+
Date.now() - cached.fetchedAt < OAuthServerImpl.CIMD_CACHE_TTL_MS) {
|
|
690
|
+
return cached.redirectUris;
|
|
691
|
+
}
|
|
692
|
+
let parsed;
|
|
693
|
+
try {
|
|
694
|
+
parsed = new URL(clientIdUrl);
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
if (parsed.protocol !== "https:")
|
|
700
|
+
return null;
|
|
701
|
+
if (isPrivateCimdHost(parsed.hostname))
|
|
702
|
+
return null;
|
|
703
|
+
try {
|
|
704
|
+
const controller = new AbortController();
|
|
705
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
706
|
+
let body;
|
|
707
|
+
try {
|
|
708
|
+
// No redirects — CIMD metadata documents must be served directly at
|
|
709
|
+
// the registered client_id URL. Following Location headers from an
|
|
710
|
+
// attacker-controlled server could bypass the isPrivateCimdHost() guard
|
|
711
|
+
// regardless of per-hop re-validation.
|
|
712
|
+
const resp = await fetch(clientIdUrl, {
|
|
713
|
+
signal: controller.signal,
|
|
714
|
+
headers: { Accept: "application/json" },
|
|
715
|
+
redirect: "error",
|
|
716
|
+
});
|
|
717
|
+
if (!resp.ok)
|
|
718
|
+
return null;
|
|
719
|
+
// Stream with size cap to prevent OOM
|
|
720
|
+
const reader = resp.body?.getReader();
|
|
721
|
+
if (!reader)
|
|
722
|
+
return null;
|
|
723
|
+
const chunks = [];
|
|
724
|
+
let total = 0;
|
|
725
|
+
while (true) {
|
|
726
|
+
const { done, value } = await reader.read();
|
|
727
|
+
if (done)
|
|
728
|
+
break;
|
|
729
|
+
total += value.length;
|
|
730
|
+
if (total > OAuthServerImpl.CIMD_MAX_BYTES) {
|
|
731
|
+
reader.cancel().catch(() => { });
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
chunks.push(value);
|
|
735
|
+
}
|
|
736
|
+
body = Buffer.concat(chunks).toString("utf8");
|
|
737
|
+
}
|
|
738
|
+
finally {
|
|
739
|
+
clearTimeout(timeout);
|
|
740
|
+
}
|
|
741
|
+
const doc = JSON.parse(body);
|
|
742
|
+
const uris = doc.redirect_uris;
|
|
743
|
+
if (!Array.isArray(uris) || uris.length === 0)
|
|
744
|
+
return null;
|
|
745
|
+
const redirectUris = uris.filter((u) => typeof u === "string");
|
|
746
|
+
if (redirectUris.length === 0)
|
|
747
|
+
return null;
|
|
748
|
+
this.cimdCache.set(clientIdUrl, { redirectUris, fetchedAt: Date.now() });
|
|
749
|
+
return redirectUris;
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
async parseAuthorizeParams(url) {
|
|
756
|
+
const responseType = url.searchParams.get("response_type");
|
|
757
|
+
const clientId = url.searchParams.get("client_id");
|
|
758
|
+
const redirectUri = url.searchParams.get("redirect_uri");
|
|
759
|
+
const codeChallenge = url.searchParams.get("code_challenge");
|
|
760
|
+
const codeChallengeMethod = url.searchParams.get("code_challenge_method");
|
|
761
|
+
const state = url.searchParams.get("state");
|
|
762
|
+
if (responseType !== "code")
|
|
763
|
+
return { error: "unsupported_response_type" };
|
|
764
|
+
if (!clientId || !redirectUri || !codeChallenge)
|
|
765
|
+
return { error: "invalid_request" };
|
|
766
|
+
if (codeChallengeMethod !== "S256")
|
|
767
|
+
return { error: "invalid_request" };
|
|
768
|
+
// CIMD: if client_id is an HTTPS URL, fetch its metadata document to get
|
|
769
|
+
// redirect_uris dynamically (SEP-991 / Claude Code v2.1.81+).
|
|
770
|
+
// Otherwise fall back to the pre-registered client map.
|
|
771
|
+
let allowedRedirectUris;
|
|
772
|
+
if (clientId.startsWith("https://")) {
|
|
773
|
+
const cimdUris = await this.fetchCimd(clientId);
|
|
774
|
+
if (!cimdUris)
|
|
775
|
+
return { error: "invalid_client" };
|
|
776
|
+
allowedRedirectUris = cimdUris;
|
|
777
|
+
// Register the client dynamically so the POST handler can look it up
|
|
778
|
+
if (!this.registeredClients.has(clientId)) {
|
|
779
|
+
this.registeredClients.set(clientId, {
|
|
780
|
+
redirectUris: cimdUris,
|
|
781
|
+
issuedAt: Date.now(),
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
const registered = this.registeredClients.get(clientId);
|
|
787
|
+
allowedRedirectUris = registered?.redirectUris;
|
|
788
|
+
}
|
|
789
|
+
if (!allowedRedirectUris?.includes(redirectUri)) {
|
|
790
|
+
return { error: "invalid_redirect_uri" };
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
clientId,
|
|
794
|
+
redirectUri,
|
|
795
|
+
codeChallenge,
|
|
796
|
+
scope: url.searchParams.get("scope") ?? DEFAULT_SCOPE,
|
|
797
|
+
state: state ?? "",
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
// ── Approval page HTML ────────────────────────────────────────────────────
|
|
801
|
+
approvalPage(opts) {
|
|
802
|
+
const e = (s) => s
|
|
803
|
+
.replace(/&/g, "&")
|
|
804
|
+
.replace(/</g, "<")
|
|
805
|
+
.replace(/>/g, ">")
|
|
806
|
+
.replace(/"/g, """)
|
|
807
|
+
.replace(/'/g, "'");
|
|
808
|
+
return `<!DOCTYPE html>
|
|
809
|
+
<html lang="en">
|
|
810
|
+
<head>
|
|
811
|
+
<meta charset="UTF-8">
|
|
812
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
813
|
+
<title>Authorize \u2014 Claude IDE Bridge</title>
|
|
814
|
+
<style>
|
|
815
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
816
|
+
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
|
817
|
+
background:#0f1117;color:#e2e8f0;display:flex;align-items:center;
|
|
818
|
+
justify-content:center;min-height:100vh;padding:2rem}
|
|
819
|
+
.card{background:#1a1d27;border:1px solid #2d3148;border-radius:12px;
|
|
820
|
+
padding:2rem;max-width:420px;width:100%;box-shadow:0 4px 32px rgba(0,0,0,.4)}
|
|
821
|
+
.logo{font-size:1.5rem;font-weight:700;color:#818cf8;margin-bottom:1.5rem}
|
|
822
|
+
h1{font-size:1.1rem;margin-bottom:.5rem}
|
|
823
|
+
.client{font-size:.9rem;color:#94a3b8;margin-bottom:1.5rem;word-break:break-all}
|
|
824
|
+
.scope{background:#12141e;border:1px solid #2d3148;border-radius:8px;
|
|
825
|
+
padding:1rem;margin-bottom:1.5rem;font-size:.875rem;color:#94a3b8}
|
|
826
|
+
.scope strong{color:#e2e8f0;display:block;margin-bottom:.5rem}
|
|
827
|
+
.item::before{content:"\u2713 ";color:#34d399}
|
|
828
|
+
.token-field{margin-bottom:1.25rem}
|
|
829
|
+
.token-field label{display:block;font-size:.8rem;color:#94a3b8;margin-bottom:.4rem}
|
|
830
|
+
.token-field input{width:100%;padding:.5rem .75rem;background:#12141e;border:1px solid #2d3148;
|
|
831
|
+
border-radius:6px;color:#e2e8f0;font-size:.875rem;font-family:monospace}
|
|
832
|
+
.token-field input.err{border-color:#f87171}
|
|
833
|
+
.token-err{color:#f87171;font-size:.8rem;margin-top:.3rem}
|
|
834
|
+
.actions{display:flex;gap:.75rem}
|
|
835
|
+
button{flex:1;padding:.65rem 1rem;border:none;border-radius:8px;
|
|
836
|
+
font-size:.95rem;font-weight:600;cursor:pointer;transition:opacity .15s}
|
|
837
|
+
button:hover{opacity:.85}
|
|
838
|
+
.approve{background:#818cf8;color:#0f1117}
|
|
839
|
+
.deny{background:#2d3148;color:#94a3b8}
|
|
840
|
+
footer{margin-top:1.25rem;font-size:.75rem;color:#475569;text-align:center}
|
|
841
|
+
</style>
|
|
842
|
+
</head>
|
|
843
|
+
<body>
|
|
844
|
+
<div class="card">
|
|
845
|
+
<div class="logo">\u29ed Claude IDE Bridge</div>
|
|
846
|
+
<h1>Authorization Request</h1>
|
|
847
|
+
<p class="client">Client: <strong>${e(opts.clientId)}</strong></p>
|
|
848
|
+
<div class="scope">
|
|
849
|
+
<strong>Requested permissions</strong>
|
|
850
|
+
<div class="item">Full MCP tool access (read, write, execute)</div>
|
|
851
|
+
</div>
|
|
852
|
+
<form method="POST" action="/oauth/authorize">
|
|
853
|
+
<input type="hidden" name="client_id" value="${e(opts.clientId)}">
|
|
854
|
+
<input type="hidden" name="redirect_uri" value="${e(opts.redirectUri)}">
|
|
855
|
+
<input type="hidden" name="code_challenge" value="${e(opts.codeChallenge)}">
|
|
856
|
+
<input type="hidden" name="scope" value="${e(opts.scope)}">
|
|
857
|
+
<input type="hidden" name="state" value="${e(opts.state)}">
|
|
858
|
+
<input type="hidden" name="csrf_nonce" value="${e(opts.csrfNonce ?? "")}">
|
|
859
|
+
<input type="hidden" name="flow_id" value="${e(opts.flowId ?? "")}">
|
|
860
|
+
<div class="token-field">
|
|
861
|
+
<label for="bridge_token">Bridge Token</label>
|
|
862
|
+
<input id="bridge_token" type="password" name="bridge_token" placeholder="Paste your bridge token"
|
|
863
|
+
class="${opts.tokenError ? "err" : ""}" autocomplete="off" required>
|
|
864
|
+
${opts.tokenError ? '<div class="token-err">Incorrect token — check your bridge token and try again.</div>' : ""}
|
|
865
|
+
</div>
|
|
866
|
+
<div class="actions">
|
|
867
|
+
<button class="approve" type="submit" name="action" value="approve">Authorize</button>
|
|
868
|
+
<button class="deny" type="submit" name="action" value="deny">Deny</button>
|
|
869
|
+
</div>
|
|
870
|
+
</form>
|
|
871
|
+
<footer>
|
|
872
|
+
Issuer: ${e(this.issuerUrl)}<br>
|
|
873
|
+
Only approve if you initiated this from your MCP client.
|
|
874
|
+
</footer>
|
|
875
|
+
</div>
|
|
876
|
+
</body>
|
|
877
|
+
</html>`;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
//# sourceMappingURL=oauth.js.map
|