one-code-extension 0.4.1 → 0.5.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/README.md +16 -5
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +58 -31
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +18 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { HttpServer } from "../config.ts";
|
|
13
|
-
import { beginInteractiveAuth, type Connection, connect } from "../client.ts";
|
|
13
|
+
import { beginInteractiveAuth, type Connection, connect, finishInteractiveAuth } from "../client.ts";
|
|
14
14
|
import { openBrowser } from "./browser.ts";
|
|
15
15
|
import { startCallbackServer } from "./callback.ts";
|
|
16
16
|
import { McpOAuthProvider } from "./provider.ts";
|
|
17
|
+
import type { OAuthServer } from "./store.ts";
|
|
17
18
|
|
|
18
19
|
export interface AuthenticateOptions {
|
|
19
20
|
server: HttpServer;
|
|
@@ -25,8 +26,8 @@ export interface AuthenticateOptions {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/** A provider for a silent (non-interactive) connect using already-stored tokens. */
|
|
28
|
-
export function silentProvider(
|
|
29
|
-
return new McpOAuthProvider({
|
|
29
|
+
export function silentProvider(server: OAuthServer, home?: string, env?: NodeJS.ProcessEnv): McpOAuthProvider {
|
|
30
|
+
return new McpOAuthProvider({ server, home, env });
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export async function authenticate(options: AuthenticateOptions): Promise<Connection> {
|
|
@@ -34,14 +35,17 @@ export async function authenticate(options: AuthenticateOptions): Promise<Connec
|
|
|
34
35
|
const callback = await startCallbackServer();
|
|
35
36
|
try {
|
|
36
37
|
const provider = new McpOAuthProvider({
|
|
37
|
-
|
|
38
|
+
server,
|
|
38
39
|
redirectUrl: callback.redirectUrl,
|
|
39
40
|
openAuthorization: (url) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
// A refused URL throws here, before any launch; the prompt waits
|
|
42
|
+
// for the opener to report whether it started.
|
|
43
|
+
void openBrowser(url.toString()).then((opened) =>
|
|
44
|
+
onPrompt?.(
|
|
45
|
+
opened
|
|
46
|
+
? `Opened your browser to authorize "${server.name}". If it didn't open, visit:\n${url}`
|
|
47
|
+
: `Open this URL to authorize "${server.name}":\n${url}`,
|
|
48
|
+
),
|
|
45
49
|
);
|
|
46
50
|
},
|
|
47
51
|
home,
|
|
@@ -51,17 +55,21 @@ export async function authenticate(options: AuthenticateOptions): Promise<Connec
|
|
|
51
55
|
const started = await beginInteractiveAuth(server, provider);
|
|
52
56
|
if ("connection" in started) return started.connection; // valid tokens already present
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
try {
|
|
59
|
+
const { code, state } = await callback.waitForCode(timeoutMs);
|
|
60
|
+
// Login-CSRF guard (RFC 8252 §8.9): the redirect must echo the state this
|
|
61
|
+
// provider issued, or an attacker-planted code could bind their account.
|
|
62
|
+
const expected = provider.expectedState();
|
|
63
|
+
if (expected !== undefined && state !== expected) {
|
|
64
|
+
throw new Error(`authorization response state mismatch for "${server.name}" — the redirect did not come from this authorization request`);
|
|
65
|
+
}
|
|
66
|
+
await finishInteractiveAuth(started.transport, code);
|
|
67
|
+
} finally {
|
|
68
|
+
await started.transport.close().catch(() => {});
|
|
60
69
|
}
|
|
61
|
-
await started.transport.finishAuth(code);
|
|
62
70
|
|
|
63
71
|
// Reconnect fresh: the provider now has stored tokens, so this is silent.
|
|
64
|
-
return await connect(server, silentProvider(server
|
|
72
|
+
return await connect(server, silentProvider(server, home, env));
|
|
65
73
|
} finally {
|
|
66
74
|
callback.close();
|
|
67
75
|
}
|
|
@@ -20,10 +20,13 @@ import type {
|
|
|
20
20
|
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
21
21
|
import { randomBytes } from "node:crypto";
|
|
22
22
|
import type { OAuthClientProvider, OAuthDiscoveryState } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
23
|
-
import { readAuth, type StoredAuth, writeAuth } from "./store.ts";
|
|
23
|
+
import { type OAuthServer, readAuth, type StoredAuth, writeAuth } from "./store.ts";
|
|
24
|
+
|
|
25
|
+
export class McpAuthenticationRequiredError extends Error {}
|
|
24
26
|
|
|
25
27
|
export interface McpOAuthProviderOptions {
|
|
26
|
-
|
|
28
|
+
/** The server the credentials are bound to (name, URL and headers). */
|
|
29
|
+
server: OAuthServer;
|
|
27
30
|
/** Loopback redirect_uri; required for an interactive flow, omitted for silent reconnect. */
|
|
28
31
|
redirectUrl?: string;
|
|
29
32
|
/** Sends the user to the authorization URL (opens a browser). Interactive flows only. */
|
|
@@ -33,7 +36,7 @@ export interface McpOAuthProviderOptions {
|
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export class McpOAuthProvider implements OAuthClientProvider {
|
|
36
|
-
private readonly
|
|
39
|
+
private readonly server: OAuthServer;
|
|
37
40
|
private readonly _redirectUrl?: string;
|
|
38
41
|
private readonly openAuthorization?: (url: URL) => void;
|
|
39
42
|
private readonly home?: string;
|
|
@@ -47,7 +50,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
47
50
|
private issuedState?: string;
|
|
48
51
|
|
|
49
52
|
constructor(options: McpOAuthProviderOptions) {
|
|
50
|
-
this.
|
|
53
|
+
this.server = options.server;
|
|
51
54
|
this._redirectUrl = options.redirectUrl;
|
|
52
55
|
this.openAuthorization = options.openAuthorization;
|
|
53
56
|
this.home = options.home;
|
|
@@ -55,17 +58,20 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
private load(): StoredAuth {
|
|
58
|
-
return (this.cache ??= readAuth(this.
|
|
61
|
+
return (this.cache ??= readAuth(this.server, this.home, this.env));
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
/** Merge a patch into the cached store and persist it. */
|
|
62
65
|
private save(patch: Partial<StoredAuth>): void {
|
|
63
66
|
this.cache = { ...this.load(), ...patch };
|
|
64
|
-
writeAuth(this.
|
|
67
|
+
writeAuth(this.server, this.cache, this.home, this.env);
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
get redirectUrl(): string | URL | undefined {
|
|
68
|
-
|
|
71
|
+
// The SDK uses an absent redirect URI to select a non-interactive grant,
|
|
72
|
+
// which skips refresh tokens. Silent reconnects still use the registered
|
|
73
|
+
// authorization-code client; only opening a browser is disallowed.
|
|
74
|
+
return this._redirectUrl ?? this.load().clientMetadata?.redirect_uris[0];
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
get clientMetadata(): OAuthClientMetadata {
|
|
@@ -113,7 +119,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
113
119
|
|
|
114
120
|
redirectToAuthorization(authorizationUrl: URL): void {
|
|
115
121
|
if (!this.openAuthorization) {
|
|
116
|
-
throw new
|
|
122
|
+
throw new McpAuthenticationRequiredError(`MCP server "${this.server.name}" needs authentication; run /mcp and choose Authenticate.`);
|
|
117
123
|
}
|
|
118
124
|
this.openAuthorization(authorizationUrl);
|
|
119
125
|
}
|
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Kept in ~/.onecode (never ~/.claude) like all One Code state, and separate
|
|
12
12
|
* from settings.json because tokens are secrets, not configuration.
|
|
13
|
+
*
|
|
14
|
+
* A record belongs to a server, not a name: the file is keyed by the name
|
|
15
|
+
* plus a sha256 of the server's URL and headers, and records the URL it was
|
|
16
|
+
* issued for. Until 2026-09-27 it was keyed by name alone, so a project
|
|
17
|
+
* `.mcp.json` server named like an authenticated user server received that
|
|
18
|
+
* server's bearer token at its own URL. A record whose URL differs is never
|
|
19
|
+
* offered, so a server whose URL or headers change signs in again.
|
|
13
20
|
*/
|
|
14
21
|
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
15
23
|
import { mkdirSync, chmodSync } from "node:fs";
|
|
16
24
|
import os from "node:os";
|
|
17
25
|
import { join } from "node:path";
|
|
@@ -20,7 +28,16 @@ import type { OAuthDiscoveryState } from "@modelcontextprotocol/sdk/client/auth.
|
|
|
20
28
|
import { readJsonFile, writeJsonAtomic } from "../../lib/atomic-write.ts";
|
|
21
29
|
import { oneCodeStateDir } from "../../lib/paths.ts";
|
|
22
30
|
|
|
31
|
+
/** The part of an http server's config its credentials are bound to. */
|
|
32
|
+
export interface OAuthServer {
|
|
33
|
+
name: string;
|
|
34
|
+
url: string;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
export interface StoredAuth {
|
|
39
|
+
/** The server URL the record was issued for; a record for another URL is never read. */
|
|
40
|
+
serverUrl?: string;
|
|
24
41
|
clientInformation?: OAuthClientInformationFull;
|
|
25
42
|
clientMetadata?: OAuthClientMetadata;
|
|
26
43
|
tokens?: OAuthTokens;
|
|
@@ -33,27 +50,28 @@ export interface StoredAuth {
|
|
|
33
50
|
* stem plus a short hash of the raw name, so two names that sanitize alike
|
|
34
51
|
* (e.g. "a/b" and "a_b") still get distinct files.
|
|
35
52
|
*/
|
|
36
|
-
function slug(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const safe =
|
|
40
|
-
return `${safe || "server"}-${hash
|
|
53
|
+
function slug(server: OAuthServer): string {
|
|
54
|
+
const headers = Object.entries(server.headers ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
55
|
+
const hash = createHash("sha256").update(JSON.stringify({ name: server.name, type: "http", url: server.url, headers })).digest("hex").slice(0, 16);
|
|
56
|
+
const safe = server.name.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48);
|
|
57
|
+
return `${safe || "server"}-${hash}.json`;
|
|
41
58
|
}
|
|
42
59
|
|
|
43
60
|
export function authStoreDir(home: string = os.homedir(), env: NodeJS.ProcessEnv = process.env): string {
|
|
44
61
|
return join(oneCodeStateDir(env, home), "mcp-auth");
|
|
45
62
|
}
|
|
46
63
|
|
|
47
|
-
function authFilePath(
|
|
48
|
-
return join(authStoreDir(home, env), slug(
|
|
64
|
+
function authFilePath(server: OAuthServer, home: string, env: NodeJS.ProcessEnv): string {
|
|
65
|
+
return join(authStoreDir(home, env), slug(server));
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
export function readAuth(
|
|
52
|
-
|
|
68
|
+
export function readAuth(server: OAuthServer, home: string = os.homedir(), env: NodeJS.ProcessEnv = process.env): StoredAuth {
|
|
69
|
+
const stored = readJsonFile<StoredAuth>(authFilePath(server, home, env));
|
|
70
|
+
return stored && stored.serverUrl === server.url ? stored : {};
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
export function writeAuth(
|
|
56
|
-
|
|
74
|
+
server: OAuthServer,
|
|
57
75
|
auth: StoredAuth,
|
|
58
76
|
home: string = os.homedir(),
|
|
59
77
|
env: NodeJS.ProcessEnv = process.env,
|
|
@@ -67,20 +85,20 @@ export function writeAuth(
|
|
|
67
85
|
} catch {
|
|
68
86
|
// Not fatal (a filesystem without modes); the file mode below still applies where it can.
|
|
69
87
|
}
|
|
70
|
-
writeJsonAtomic(authFilePath(
|
|
88
|
+
writeJsonAtomic(authFilePath(server, home, env), { ...auth, serverUrl: server.url }, { mode: 0o600 });
|
|
71
89
|
}
|
|
72
90
|
|
|
73
91
|
/** Merge a partial update into the stored record. */
|
|
74
92
|
export function updateAuth(
|
|
75
|
-
|
|
93
|
+
server: OAuthServer,
|
|
76
94
|
patch: Partial<StoredAuth>,
|
|
77
95
|
home: string = os.homedir(),
|
|
78
96
|
env: NodeJS.ProcessEnv = process.env,
|
|
79
97
|
): void {
|
|
80
|
-
writeAuth(
|
|
98
|
+
writeAuth(server, { ...readAuth(server, home, env), ...patch }, home, env);
|
|
81
99
|
}
|
|
82
100
|
|
|
83
|
-
/** True when a usable (non-empty) access token is stored. */
|
|
84
|
-
export function hasStoredTokens(
|
|
85
|
-
return Boolean(readAuth(
|
|
101
|
+
/** True when a usable (non-empty) access token is stored for this server. */
|
|
102
|
+
export function hasStoredTokens(server: OAuthServer, home: string = os.homedir(), env: NodeJS.ProcessEnv = process.env): boolean {
|
|
103
|
+
return Boolean(readAuth(server, home, env).tokens?.access_token);
|
|
86
104
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Key decoding for the /mcp panel (pure), named through `lib/key-input.ts`
|
|
3
|
+
* so every terminal encoding of a key decodes alike.
|
|
3
4
|
*
|
|
4
5
|
* The panel is a two-level menu (server list → server detail with a numbered
|
|
5
6
|
* action list), navigated with the arrows and Enter, Esc to go back/close. A
|
|
@@ -7,28 +8,28 @@
|
|
|
7
8
|
* other printable input is ignored.
|
|
8
9
|
*/
|
|
9
10
|
|
|
11
|
+
import { keyId } from "../../lib/key-input.ts";
|
|
12
|
+
|
|
10
13
|
export type McpKey =
|
|
11
14
|
| { kind: "up" | "down" | "enter" | "back" | "close" }
|
|
12
15
|
| { kind: "digit"; value: number };
|
|
13
16
|
|
|
14
17
|
export function decodeMcpKey(data: string): McpKey | undefined {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
case "
|
|
18
|
+
const id = keyId(data);
|
|
19
|
+
switch (id) {
|
|
20
|
+
case "up":
|
|
18
21
|
return { kind: "up" };
|
|
19
|
-
case "
|
|
20
|
-
case "\x1bOB":
|
|
22
|
+
case "down":
|
|
21
23
|
return { kind: "down" };
|
|
22
|
-
case "
|
|
23
|
-
case "\n":
|
|
24
|
+
case "enter":
|
|
24
25
|
return { kind: "enter" };
|
|
25
|
-
case "
|
|
26
|
+
case "escape":
|
|
26
27
|
return { kind: "back" };
|
|
27
|
-
case "
|
|
28
|
+
case "ctrl+c":
|
|
28
29
|
return { kind: "close" };
|
|
29
30
|
default:
|
|
30
31
|
break;
|
|
31
32
|
}
|
|
32
|
-
if (
|
|
33
|
+
if (id && id.length === 1 && id >= "1" && id <= "9") return { kind: "digit", value: Number(id) };
|
|
33
34
|
return undefined;
|
|
34
35
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Only width-1 glyphs are used so cutPlainText's code-point count stays exact.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { cutPlainText, padPlainText, panelTopRule, type RenderBlock as Block, windowBlocks } from "../../lib/tui-render.ts";
|
|
11
|
+
import { cutPlainText, hintLines, padPlainText, panelTopRule, type RenderBlock as Block, windowBlocks } from "../../lib/tui-render.ts";
|
|
12
12
|
import { actionsFor, type McpEntry, statusText } from "./model.ts";
|
|
13
13
|
import { clampMcpCursor, detailEntry, type McpPanelState } from "./state.ts";
|
|
14
14
|
|
|
@@ -106,19 +106,23 @@ export function renderMcpPanel(input: McpRenderInput, paint: McpPaint): string[]
|
|
|
106
106
|
clampMcpCursor(state, entries);
|
|
107
107
|
|
|
108
108
|
const out: string[] = [panelTopRule(paint.fg, width)];
|
|
109
|
-
|
|
109
|
+
// Hints wrap at their separators (hintLines); the list budget counts the rows.
|
|
110
|
+
const wrapFooter = (text: string) => hintLines(text, width - 1, " ");
|
|
111
|
+
let footer: string[];
|
|
110
112
|
|
|
111
113
|
if (state.detail) {
|
|
112
114
|
out.push("");
|
|
113
115
|
const detail = detailLines(input, paint);
|
|
114
116
|
out.push(...detail.lines);
|
|
115
|
-
footer = detail.footer;
|
|
117
|
+
footer = wrapFooter(detail.footer);
|
|
116
118
|
} else {
|
|
117
119
|
out.push(` ${paint.bold("Manage MCP servers")}`);
|
|
118
120
|
const count = `${entries.length} server${entries.length === 1 ? "" : "s"}${input.settled ? "" : " · connecting…"}`;
|
|
119
121
|
out.push(paint.fg("dim", cutPlainText(` ${count}`, width - 1)));
|
|
120
122
|
|
|
121
|
-
|
|
123
|
+
footer = wrapFooter("↑/↓ to navigate · Enter to confirm · Esc to cancel");
|
|
124
|
+
const noticeLines = input.notices.reduce((sum, notice) => sum + notice.split("\n").length, 0);
|
|
125
|
+
const footerReserve = 2 + footer.length + noticeLines + 1;
|
|
122
126
|
const budget = Math.max(1, height - out.length - footerReserve);
|
|
123
127
|
if (entries.length === 0) {
|
|
124
128
|
out.push("", paint.fg("dim", " No MCP servers configured. Add them to .mcp.json or ~/.claude.json."));
|
|
@@ -129,13 +133,12 @@ export function renderMcpPanel(input: McpRenderInput, paint: McpPaint): string[]
|
|
|
129
133
|
}
|
|
130
134
|
out.push("");
|
|
131
135
|
out.push(paint.fg("dim", cutPlainText(" Servers are configured in .mcp.json or ~/.claude.json.", width - 1)));
|
|
132
|
-
footer = "↑/↓ to navigate · Enter to confirm · Esc to cancel";
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
for (const notice of input.notices) {
|
|
136
139
|
for (const line of notice.split("\n")) out.push(paint.fg("warning", cutPlainText(` ${line}`, width - 1)));
|
|
137
140
|
}
|
|
138
141
|
out.push("");
|
|
139
|
-
out.push(paint.fg("dim",
|
|
142
|
+
for (const line of footer) out.push(paint.fg("dim", line));
|
|
140
143
|
return out;
|
|
141
144
|
}
|
package/extensions/mcp/schema.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* anything exotic degrades to a permissive object that passes arguments through.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { crc32 } from "node:zlib";
|
|
10
11
|
import { type TSchema, Type } from "typebox";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -67,9 +68,8 @@ function descriptionOptions(schema: JsonSchema): Record<string, unknown> {
|
|
|
67
68
|
|
|
68
69
|
function convertLeaf(schema: JsonSchema): TSchema | undefined {
|
|
69
70
|
const options = descriptionOptions(schema);
|
|
70
|
-
const type = Array.isArray(schema.type) ? schema.type.find((t) => t !== "null") : schema.type;
|
|
71
71
|
|
|
72
|
-
switch (type) {
|
|
72
|
+
switch (schema.type) {
|
|
73
73
|
case "string":
|
|
74
74
|
return Type.String(options);
|
|
75
75
|
case "number":
|
|
@@ -78,10 +78,10 @@ function convertLeaf(schema: JsonSchema): TSchema | undefined {
|
|
|
78
78
|
return Type.Integer(options);
|
|
79
79
|
case "boolean":
|
|
80
80
|
return Type.Boolean(options);
|
|
81
|
+
case "null":
|
|
82
|
+
return Type.Null(options);
|
|
81
83
|
case "array":
|
|
82
84
|
return Type.Array(schema.items ? jsonSchemaToTypeBox(schema.items) : Type.Unknown(), options);
|
|
83
|
-
case "object":
|
|
84
|
-
return jsonSchemaToTypeBox(schema);
|
|
85
85
|
default:
|
|
86
86
|
return undefined;
|
|
87
87
|
}
|
|
@@ -108,7 +108,17 @@ export function jsonSchemaToTypeBox(schema: JsonSchema | undefined): TSchema {
|
|
|
108
108
|
return Type.Unknown();
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
if (schema.
|
|
111
|
+
if (Array.isArray(schema.type) && schema.type.length > 0) {
|
|
112
|
+
if (schema.type.length === 1) return jsonSchemaToTypeBox({ ...schema, type: schema.type[0] });
|
|
113
|
+
// TypeBox emits anyOf, which pi's provider converters understand. Keep
|
|
114
|
+
// the description on the union, not on each of its generated branches.
|
|
115
|
+
return Type.Union(
|
|
116
|
+
schema.type.map((type) => jsonSchemaToTypeBox({ ...schema, type, description: undefined })),
|
|
117
|
+
options,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (schema.type === "object" || (schema.type === undefined && schema.properties)) {
|
|
112
122
|
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
113
123
|
const properties: Record<string, TSchema> = {};
|
|
114
124
|
for (const [key, value] of Object.entries(schema.properties ?? {})) {
|
|
@@ -176,6 +186,12 @@ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
|
176
186
|
* on every later one, since the block stays in the history. So the bytes are
|
|
177
187
|
* checked against a small magic-number allow-list here and `mimeType` is
|
|
178
188
|
* derived from them, never trusted; a block that fails becomes a text note.
|
|
189
|
+
* A valid signature is not enough: a PNG signature followed by garbage passed
|
|
190
|
+
* the sniff and still poisoned the session. So the container is walked to its
|
|
191
|
+
* end too (`wholeImageProblem`): PNG chunks with their CRCs from IHDR to IEND,
|
|
192
|
+
* JPEG segments to the scan and the end-of-image marker, the GIF trailer, the
|
|
193
|
+
* WebP RIFF length. It is not a pixel decode, so a crafted image with a valid
|
|
194
|
+
* container can still fail at the provider; a corrupt or truncated one cannot.
|
|
179
195
|
*/
|
|
180
196
|
/** Base64 length that decodes to just over the byte ceiling (4 base64 chars per 3 bytes). */
|
|
181
197
|
const MAX_IMAGE_BASE64_LENGTH = Math.ceil((MAX_IMAGE_BYTES * 4) / 3);
|
|
@@ -197,9 +213,92 @@ export function validateImageData(
|
|
|
197
213
|
if (head.length === 0) return { ok: false, reason: "empty after base64 decode", bytes: 0 };
|
|
198
214
|
const mime = sniffImageMime(head);
|
|
199
215
|
if (!mime) return { ok: false, reason: `unrecognised image format (claimed ${claimedMime ?? "none"})`, bytes: approxBytes };
|
|
216
|
+
const bytes = Buffer.from(data, "base64");
|
|
217
|
+
const problem = wholeImageProblem(bytes, mime);
|
|
218
|
+
if (problem) return { ok: false, reason: problem, bytes: bytes.length };
|
|
200
219
|
return { ok: true, mimeType: mime };
|
|
201
220
|
}
|
|
202
221
|
|
|
222
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
223
|
+
|
|
224
|
+
/** A JPEG end-of-image marker. */
|
|
225
|
+
const JPEG_EOI = Buffer.from([0xff, 0xd9]);
|
|
226
|
+
|
|
227
|
+
/** The end of `buf` with trailing zero padding dropped (some encoders pad after the end marker). */
|
|
228
|
+
function paddedEnd(buf: Buffer): number {
|
|
229
|
+
let end = buf.length;
|
|
230
|
+
while (end > 0 && buf[end - 1] === 0x00) end--;
|
|
231
|
+
return end;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Why the image's container is not whole, or undefined when it is. Pure. */
|
|
235
|
+
export function wholeImageProblem(buf: Buffer, mime: string): string | undefined {
|
|
236
|
+
if (mime === "image/png") {
|
|
237
|
+
if (buf.length < 8 || !buf.subarray(0, 8).equals(PNG_SIGNATURE)) return "corrupt PNG signature";
|
|
238
|
+
let offset = 8;
|
|
239
|
+
let first = true;
|
|
240
|
+
let sawData = false;
|
|
241
|
+
for (;;) {
|
|
242
|
+
if (offset + 12 > buf.length) return "truncated PNG (no IEND chunk)";
|
|
243
|
+
const length = buf.readUInt32BE(offset);
|
|
244
|
+
const type = buf.toString("latin1", offset + 4, offset + 8);
|
|
245
|
+
if (offset + 12 + length > buf.length) return `truncated PNG (the ${type} chunk runs past the data)`;
|
|
246
|
+
if (crc32(buf.subarray(offset + 4, offset + 8 + length)) !== buf.readUInt32BE(offset + 8 + length)) return `corrupt PNG (bad ${type} chunk checksum)`;
|
|
247
|
+
if (first) {
|
|
248
|
+
if (type !== "IHDR" || length !== 13) return "corrupt PNG (IHDR is not the first chunk)";
|
|
249
|
+
if (buf.readUInt32BE(offset + 8) === 0 || buf.readUInt32BE(offset + 12) === 0) return "corrupt PNG (zero width or height)";
|
|
250
|
+
first = false;
|
|
251
|
+
}
|
|
252
|
+
if (type === "IDAT") sawData = true;
|
|
253
|
+
offset += 12 + length;
|
|
254
|
+
if (type === "IEND") return sawData ? undefined : "corrupt PNG (no image data)";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (mime === "image/jpeg") {
|
|
258
|
+
let offset = 2;
|
|
259
|
+
for (;;) {
|
|
260
|
+
if (offset + 2 > buf.length) return "truncated JPEG (no image scan)";
|
|
261
|
+
if (buf[offset] !== 0xff) return "corrupt JPEG (bad segment marker)";
|
|
262
|
+
const marker = buf[offset + 1];
|
|
263
|
+
if (marker === 0xff) {
|
|
264
|
+
offset++; // fill byte
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (marker === 0xda) break; // start of scan: entropy-coded data follows
|
|
268
|
+
if (marker === 0xd9) return "corrupt JPEG (ends before its image scan)";
|
|
269
|
+
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
270
|
+
offset += 2;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (offset + 4 > buf.length) return "truncated JPEG";
|
|
274
|
+
const length = buf.readUInt16BE(offset + 2);
|
|
275
|
+
if (length < 2 || offset + 2 + length > buf.length) return "truncated JPEG (a segment runs past the data)";
|
|
276
|
+
offset += 2 + length;
|
|
277
|
+
}
|
|
278
|
+
// The EOI marker anywhere after the scan: scan data escapes every 0xFF, and
|
|
279
|
+
// bytes after EOI (a vendor trailer, a Motion Photo payload) are allowed.
|
|
280
|
+
return buf.indexOf(JPEG_EOI, offset + 2) !== -1 ? undefined : "truncated JPEG (no end-of-image marker)";
|
|
281
|
+
}
|
|
282
|
+
if (mime === "image/gif") {
|
|
283
|
+
const header = buf.toString("latin1", 0, 6);
|
|
284
|
+
if (header !== "GIF87a" && header !== "GIF89a") return "corrupt GIF header";
|
|
285
|
+
if (buf.length < 14) return "truncated GIF";
|
|
286
|
+
if (buf.readUInt16LE(6) === 0 || buf.readUInt16LE(8) === 0) return "corrupt GIF (zero width or height)";
|
|
287
|
+
const end = paddedEnd(buf);
|
|
288
|
+
return buf[end - 1] === 0x3b ? undefined : "truncated GIF (no trailer)";
|
|
289
|
+
}
|
|
290
|
+
if (mime === "image/webp") {
|
|
291
|
+
if (buf.length < 20) return "truncated WebP";
|
|
292
|
+
const riffEnd = buf.readUInt32LE(4) + 8;
|
|
293
|
+
if (riffEnd > buf.length) return "truncated WebP (shorter than its RIFF length)";
|
|
294
|
+
const chunk = buf.toString("latin1", 12, 16);
|
|
295
|
+
if (chunk !== "VP8 " && chunk !== "VP8L" && chunk !== "VP8X") return "corrupt WebP (no image chunk)";
|
|
296
|
+
if (20 + buf.readUInt32LE(16) > riffEnd) return "truncated WebP (its image chunk runs past the data)";
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
return `unsupported image type ${mime}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
203
302
|
/** Magic-number sniff for the formats Anthropic accepts. Returns undefined for anything else. */
|
|
204
303
|
function sniffImageMime(buf: Buffer): string | undefined {
|
|
205
304
|
if (buf.length >= 8 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return "image/png";
|