@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -38,7 +38,7 @@ const PROVIDER_ALIASES: Record<string, string> = {
|
|
|
38
38
|
type Row = readonly [id: string, contextWindow?: number | null, maxTokens?: number | null, input?: string | null, reasoning?: 0 | 1 | null, wireModelId?: string | null, costInput?: number | null, costOutput?: number | null, costCacheRead?: number | null, costCacheWrite?: number | null];
|
|
39
39
|
const DATA: Record<string, readonly Row[]> = {
|
|
40
40
|
"amazon-bedrock": [["anthropic.claude-3-5-haiku-20241022-v1:0",200000,8192,"text,image",0,null,0.8,4,0.08,1],["anthropic.claude-3-5-sonnet-20240620-v1:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["anthropic.claude-3-5-sonnet-20241022-v2:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["anthropic.claude-3-haiku-20240307-v1:0",200000,4096,"text,image",0,null,0.25,1.25,0,0],["anthropic.claude-3-opus-20240229-v1:0",200000,4096,"text,image",0,null,15,75,0,0],["anthropic.claude-3-sonnet-20240229-v1:0",200000,4096,"text,image",0,null,3,15,0,0],["anthropic.claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["au.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1,5,0.1,1.25],["au.anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,16.5,82.5,0.5,6.25],["au.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["au.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["au.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["au.anthropic.claude-sonnet-4-6",1000000,128000,"text,image",1,null,3.3,16.5,0.33,4.125],["au.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["cohere.command-r-plus-v1:0",128000,4096,"text",0,null,3,15,0,0],["cohere.command-r-v1:0",128000,4096,"text",0,null,0.5,1.5,0,0],["deepseek.v3-v1:0",163840,81920,"text",1,null,0.58,1.68,0,0],["deepseek.v3.2",163840,81920,"text",1,null,0.62,1.85,0,0],["deepseek.v3.2-v1:0",163840,81920,"text",1,null,0.62,1.85,0,0],["eu.anthropic.claude-3-5-haiku-20241022-v1:0",200000,8192,"text,image",0,null,0.8,4,0.08,1],["eu.anthropic.claude-3-5-sonnet-20240620-v1:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["eu.anthropic.claude-3-5-sonnet-20241022-v2:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["eu.anthropic.claude-3-7-sonnet-20250219-v1:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["eu.anthropic.claude-3-haiku-20240307-v1:0",200000,4096,"text,image",0,null,0.25,1.25,0,0],["eu.anthropic.claude-3-opus-20240229-v1:0",200000,4096,"text,image",0,null,15,75,0,0],["eu.anthropic.claude-3-sonnet-20240229-v1:0",200000,4096,"text,image",0,null,3,15,0,0],["eu.anthropic.claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["eu.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1.1,5.5,0.11,1.375],["eu.anthropic.claude-opus-4-1-20250805-v1:0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["eu.anthropic.claude-opus-4-20250514-v1:0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["eu.anthropic.claude-opus-4-5-20251101-v1:0",200000,64000,"text,image",1,null,5.5,27.5,0.55,6.875],["eu.anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["eu.anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5.5,27.5,0.55,6.875],["eu.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5.5,27.5,0.55,6.875],["eu.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5.5,27.5,0.55,6.875],["eu.anthropic.claude-sonnet-4-20250514-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["eu.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["eu.anthropic.claude-sonnet-4-6",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["eu.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["global.amazon.nova-2-lite-v1:0",128000,4096,"text,image",1,null,0.33,2.75,0,0],["global.anthropic.claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["global.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1,5,0.1,1.25],["global.anthropic.claude-opus-4-5-20251101-v1:0",200000,64000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-sonnet-4-20250514-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["global.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["global.anthropic.claude-sonnet-4-6",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["global.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["google.gemma-3-27b-it",202752,8192,"text,image",0,null,0.12,0.2,0,0],["google.gemma-3-4b-it",128000,4096,"text,image",0,null,0.04,0.08,0,0],["jp.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1,5,0.1,1.25],["jp.anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["jp.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["jp.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["jp.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["jp.anthropic.claude-sonnet-4-6",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["jp.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["meta.llama3-1-405b-instruct-v1:0",128000,4096,"text",0,null,2.4,2.4,0,0],["meta.llama3-1-70b-instruct-v1:0",128000,4096,"text",0,null,0.72,0.72,0,0],["meta.llama3-1-8b-instruct-v1:0",128000,4096,"text",0,null,0.22,0.22,0,0],["minimax.minimax-m2",204608,128000,"text",1,null,0.3,1.2,0,0],["minimax.minimax-m2.1",204800,131072,"text",1,null,0.3,1.2,0,0],["minimax.minimax-m2.5",196608,98304,"text",1,null,0.3,1.2,0,0],["mistral.devstral-2-123b",256000,8192,"text",0,null,0.4,2,0,0],["mistral.magistral-small-2509",128000,40000,"text,image",1,null,0.5,1.5,0,0],["mistral.ministral-3-14b-instruct",128000,4096,"text",0,null,0.2,0.2,0,0],["mistral.ministral-3-3b-instruct",256000,8192,"text,image",0,null,0.1,0.1,0,0],["mistral.ministral-3-8b-instruct",128000,4096,"text",0,null,0.15,0.15,0,0],["mistral.mistral-large-2402-v1:0",128000,4096,"text",0,null,0.5,1.5,0,0],["mistral.mistral-large-3-675b-instruct",256000,8192,"text,image",0,null,0.5,1.5,0,0],["mistral.pixtral-large-2502-v1:0",128000,8192,"text,image",0,null,2,6,0,0],["mistral.voxtral-mini-3b-2507",128000,4096,"text",0,null,0.04,0.04,0,0],["mistral.voxtral-small-24b-2507",32000,8192,"text",0,null,0.15,0.35,0,0],["moonshot.kimi-k2-thinking",262143,16000,"text",1,null,0.6,2.5,0,0],["moonshotai.kimi-k2.5",262143,16000,"text,image",1,null,0.6,3,0,0],["nvidia.nemotron-nano-12b-v2",128000,4096,"text,image",0,null,0.2,0.6,0,0],["nvidia.nemotron-nano-3-30b",128000,4096,"text",1,null,0.06,0.24,0,0],["nvidia.nemotron-nano-9b-v2",128000,4096,"text",0,null,0.06,0.23,0,0],["nvidia.nemotron-super-3-120b",262144,131072,"text",1,null,0.15,0.65,0,0],["openai.gpt-5.4",272000,128000,"text,image",1,null,2.75,16.5,0.275,0],["openai.gpt-5.5",272000,128000,"text,image",1,null,5.5,33,0.55,0],["openai.gpt-5.6-luna",373000,128000,"text,image",1,null,1,6,0.1,1.25],["openai.gpt-5.6-sol",373000,128000,"text,image",1,null,5,30,0.5,6.25],["openai.gpt-5.6-terra",373000,128000,"text,image",1,null,2.5,15,0.25,3.125],["openai.gpt-oss-120b",128000,16384,"text",1,null,0.15,0.6,0,0],["openai.gpt-oss-120b-1:0",128000,16384,"text",1,null,0.15,0.6,0,0],["openai.gpt-oss-20b",128000,16384,"text",1,null,0.07,0.3,0,0],["openai.gpt-oss-20b-1:0",128000,16384,"text",1,null,0.07,0.3,0,0],["openai.gpt-oss-safeguard-120b",128000,16384,"text",0,null,0.15,0.6,0,0],["openai.gpt-oss-safeguard-20b",128000,16384,"text",0,null,0.07,0.2,0,0],["qwen.qwen3-235b-a22b-2507-v1:0",262144,131072,"text",0,null,0.22,0.88,0,0],["qwen.qwen3-32b-v1:0",16384,16384,"text",1,null,0.15,0.6,0,0],["qwen.qwen3-coder-30b-a3b-v1:0",262144,131072,"text",0,null,0.15,0.6,0,0],["qwen.qwen3-coder-480b-a35b-v1:0",131072,65536,"text",0,null,0.22,1.8,0,0],["qwen.qwen3-coder-next",131072,65536,"text",1,null,0.22,1.8,0,0],["qwen.qwen3-next-80b-a3b",262000,262000,"text",0,null,0.14,1.4,0,0],["qwen.qwen3-vl-235b-a22b",262000,262000,"text,image",0,null,0.3,1.5,0,0],["us.amazon.nova-lite-v1:0",300000,8192,"text,image",0,null,0.06,0.24,0.015,0],["us.amazon.nova-micro-v1:0",128000,8192,"text",0,null,0.035,0.14,0.00875,0],["us.amazon.nova-premier-v1:0",1000000,16384,"text,image",1,null,2.5,12.5,0,0],["us.amazon.nova-pro-v1:0",300000,8192,"text,image",0,null,0.8,3.2,0.2,0],["us.anthropic.claude-3-7-sonnet-20250219-v1:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["us.anthropic.claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["us.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1,5,0.1,1.25],["us.anthropic.claude-opus-4-1-20250805-v1:0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["us.anthropic.claude-opus-4-20250514-v1:0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["us.anthropic.claude-opus-4-5-20251101-v1:0",200000,64000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-sonnet-4-20250514-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["us.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["us.anthropic.claude-sonnet-4-6",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["us.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["us.deepseek.r1-v1:0",128000,32768,"text",1,null,1.35,5.4,0,0],["us.meta.llama3-2-11b-instruct-v1:0",128000,4096,"text,image",0,null,0.16,0.16,0,0],["us.meta.llama3-2-1b-instruct-v1:0",131000,4096,"text",0,null,0.1,0.1,0,0],["us.meta.llama3-2-3b-instruct-v1:0",131000,4096,"text",0,null,0.15,0.15,0,0],["us.meta.llama3-2-90b-instruct-v1:0",128000,4096,"text,image",0,null,0.72,0.72,0,0],["us.meta.llama3-3-70b-instruct-v1:0",128000,4096,"text",0,null,0.72,0.72,0,0],["us.meta.llama4-maverick-17b-instruct-v1:0",1000000,16384,"text,image",0,null,0.24,0.97,0,0],["us.meta.llama4-scout-17b-instruct-v1:0",3500000,16384,"text,image",0,null,0.17,0.66,0,0],["writer.palmyra-x4-v1:0",122880,8192,"text",1,null,2.5,10,0,0],["writer.palmyra-x5-v1:0",1040000,8192,"text",1,null,0.6,6,0,0],["xai.grok-4.3",1000000,131072,"text,image",1,null,1.25,2.5,0.2,0],["zai.glm-4.7",204800,131072,"text",1,null,0.6,2.2,0,0],["zai.glm-4.7-flash",200000,131072,"text",1,null,0.07,0.4,0,0],["zai.glm-5",202752,101376,"text",1,null,1,3.2,0,0]],
|
|
41
|
-
"anthropic": [["claude-3-5-sonnet-20240620",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-5-sonnet-20241022",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-haiku-20240307",200000,4096,"text,image",0,null,0.25,1.25,0.03,0.3],["claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["claude-haiku-4-5",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-haiku-4-5-20251001",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-opus-4-0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1-20250805",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-20250514",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-5",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-5-20251101",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6[1m]",1000000,128000,"text,image",1,"claude-opus-4-6",5,25,0.5,6.25],["claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-7[1m]",1000000,128000,"text,image",1,"claude-opus-4-7",5,25,0.5,6.25],["claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-8[1m]",1000000,128000,"text,image",1,"claude-opus-4-8",5,25,0.5,6.25],["claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-sonnet-4-0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-20250514",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5-20250929",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6",1000000,128000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6[1m]",1000000,64000,"text,image",1,"claude-sonnet-4-6",3,15,0.3,3.75],["claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5]],
|
|
41
|
+
"anthropic": [["claude-3-5-sonnet-20240620",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-5-sonnet-20241022",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-haiku-20240307",200000,4096,"text,image",0,null,0.25,1.25,0.03,0.3],["claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["claude-fable-5-1",1000000,128000,"text,image",1,null,10,50,0.25,12.5],["claude-haiku-4-5",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-haiku-4-5-20251001",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-opus-4-0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1-20250805",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-20250514",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-5",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-5-20251101",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6[1m]",1000000,128000,"text,image",1,"claude-opus-4-6",5,25,0.5,6.25],["claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-7[1m]",1000000,128000,"text,image",1,"claude-opus-4-7",5,25,0.5,6.25],["claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-8[1m]",1000000,128000,"text,image",1,"claude-opus-4-8",5,25,0.5,6.25],["claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-sonnet-4-0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-20250514",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5-20250929",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6",1000000,128000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6[1m]",1000000,64000,"text,image",1,"claude-sonnet-4-6",3,15,0.3,3.75],["claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5]],
|
|
42
42
|
"azure-openai": [["gpt-4.1",1047576,32768,"text,image",0,null,2,8,0.5,0],["gpt-4o",128000,16384,"text,image",0,null,2.5,10,1.25,0],["gpt-4o-mini",128000,16384,"text,image",0,null,0.15,0.6,0.075,0],["o3",200000,100000,"text,image",1,null,2,8,0.5,0],["o3-mini",200000,100000,"text",1,null,1.1,4.4,0.55,0]],
|
|
43
43
|
"cerebras": [["gemma-4-31b",131072,40960,"text,image",1,null,0.99,1.49,0,0],["gpt-oss-120b",131072,40960,"text",1,null,0.35,0.75,0,0],["llama3.1-8b",32000,8000,"text",0,null,0.1,0.1,0,0],["qwen-3-235b-a22b-instruct-2507",131000,32000,"text",0,null,0.6,1.2,0,0],["qwen-3-coder-480b",131072,32768,"text",0,null,0,0,0,0],["zai-glm-4.6",131072,32768,"text",0,null,0,0,0,0],["zai-glm-4.7",131072,40960,"text",1,null,2.25,2.75,2.25,0]],
|
|
44
44
|
"deepseek": [["deepseek-v4-flash",1048576,384000,"text",1,null,0.14,0.28,0.0028,0],["deepseek-v4-pro",1048576,384000,"text",1,null,0.435,0.87,0.003625,0]],
|
|
@@ -26,7 +26,7 @@ import { isIntegrationClientId, type IntegrationClientId } from "./registry";
|
|
|
26
26
|
*
|
|
27
27
|
* This union is re-declared, not imported, in two other places -- the management
|
|
28
28
|
* route envelope and the GUI adapter -- because neither imports across that
|
|
29
|
-
* boundary. `tests/integrations-journal.test.ts` asserts the three agree, since
|
|
29
|
+
* boundary. `tests/clients/integrations-journal.test.ts` asserts the three agree, since
|
|
30
30
|
* nothing else can: a kind persisted here and missing there renders as a raw
|
|
31
31
|
* key with no type error anywhere.
|
|
32
32
|
*/
|
|
@@ -64,6 +64,44 @@ export interface JournalEntry {
|
|
|
64
64
|
|
|
65
65
|
export const SNAPSHOT_RETENTION = 10;
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* A deletion, expressed as an APPEND.
|
|
69
|
+
*
|
|
70
|
+
* The alternative -- rewriting journal.jsonl without the row -- breaks all three
|
|
71
|
+
* things this file's header promises. `appendOperation` commits and nothing
|
|
72
|
+
* else, so a read-modify-write would race any concurrent append; a torn write
|
|
73
|
+
* would truncate the whole log rather than one trailing line, which
|
|
74
|
+
* `listOperations` is built to tolerate; and no lock covers this file, because
|
|
75
|
+
* append-only never needed one (writer-lock.ts guards `<configPath>.lock`,
|
|
76
|
+
* which is a client config, not this).
|
|
77
|
+
*
|
|
78
|
+
* "journal rows always survive" (pruneSnapshots below) is a promise about
|
|
79
|
+
* RETENTION, not about the user. An operator deleting their own row is not
|
|
80
|
+
* retention, and the physical line does in fact survive -- this record is laid
|
|
81
|
+
* over it.
|
|
82
|
+
*/
|
|
83
|
+
export interface JournalTombstone {
|
|
84
|
+
/** opId this row retires. */
|
|
85
|
+
tombstone: string;
|
|
86
|
+
at: string;
|
|
87
|
+
/** Management principal that asked. Never a token, never a path. */
|
|
88
|
+
by: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isTombstone(value: unknown): value is JournalTombstone {
|
|
92
|
+
return typeof value === "object" && value !== null
|
|
93
|
+
&& typeof (value as { tombstone?: unknown }).tombstone === "string";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Retire one operation. Append-only, exactly like `appendOperation`. */
|
|
97
|
+
export function appendTombstone(
|
|
98
|
+
record: JournalTombstone,
|
|
99
|
+
dir: string = integrationsDir(),
|
|
100
|
+
): void {
|
|
101
|
+
ensureDir(journalPath(dir));
|
|
102
|
+
appendFileSync(journalPath(dir), `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
103
|
+
}
|
|
104
|
+
|
|
67
105
|
/**
|
|
68
106
|
* Does the file on disk still hold what this operation left behind?
|
|
69
107
|
*
|
|
@@ -159,16 +197,34 @@ export function listOperations(
|
|
|
159
197
|
return [];
|
|
160
198
|
}
|
|
161
199
|
const rows: JournalEntry[] = [];
|
|
200
|
+
/*
|
|
201
|
+
* Collected in the SAME pass, before any filtering. A tombstone carries no
|
|
202
|
+
* clientId -- it names an opId -- so a pass that filtered by client first
|
|
203
|
+
* would drop the tombstone and resurrect the row on the per-client route
|
|
204
|
+
* while the global route hid it. The two routes read the same log and must
|
|
205
|
+
* agree.
|
|
206
|
+
*/
|
|
207
|
+
const retired = new Set<string>();
|
|
162
208
|
for (const line of raw.split("\n")) {
|
|
163
209
|
if (!line.trim()) continue;
|
|
164
210
|
try {
|
|
165
|
-
const parsed = JSON.parse(line)
|
|
166
|
-
if (
|
|
211
|
+
const parsed: unknown = JSON.parse(line);
|
|
212
|
+
if (isTombstone(parsed)) {
|
|
213
|
+
retired.add(parsed.tombstone);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const entry = parsed as JournalEntry;
|
|
217
|
+
if (!clientId || entry.clientId === clientId) rows.push(entry);
|
|
167
218
|
} catch {
|
|
168
219
|
// Torn line from an interrupted append; the rest of the log is still good.
|
|
169
220
|
}
|
|
170
221
|
}
|
|
171
|
-
|
|
222
|
+
/*
|
|
223
|
+
* Filter AFTER the whole file is read, never during. A tombstone is always
|
|
224
|
+
* appended after the row it retires, so an in-loop check would miss every one.
|
|
225
|
+
*/
|
|
226
|
+
const live = retired.size === 0 ? rows : rows.filter(row => !retired.has(row.opId));
|
|
227
|
+
return live.reverse().slice(0, limit);
|
|
172
228
|
}
|
|
173
229
|
|
|
174
230
|
export function findOperation(opId: string, dir: string = integrationsDir()): JournalEntry | null {
|
|
@@ -211,6 +267,11 @@ export function countSnapshots(
|
|
|
211
267
|
* Keep the newest N snapshot files per client; journal rows always survive.
|
|
212
268
|
* Structured rather than throwing or swallowing: a swallowed failure would let
|
|
213
269
|
* credential-bearing snapshots pile up while every operation reported success.
|
|
270
|
+
*
|
|
271
|
+
* A user-deleted row no longer occupies a retention slot: `listOperations`
|
|
272
|
+
* hides retired rows, so the keep window below slides down by one for each
|
|
273
|
+
* deletion. The direction is safe -- an older backup is kept rather than
|
|
274
|
+
* collected -- but "ten backups per client" counts LIVE rows, not operations.
|
|
214
275
|
*/
|
|
215
276
|
export function pruneSnapshots(
|
|
216
277
|
clientId: IntegrationClientId,
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import {
|
|
14
14
|
appendOperation,
|
|
15
|
+
appendTombstone,
|
|
15
16
|
captureSnapshot,
|
|
16
17
|
clearPruneFailure,
|
|
17
18
|
countSnapshots,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
readMaintenance,
|
|
23
24
|
readSnapshot,
|
|
24
25
|
type JournalEntry,
|
|
26
|
+
type JournalTombstone,
|
|
25
27
|
type MaintenanceState,
|
|
26
28
|
type SnapshotRef,
|
|
27
29
|
} from "./journal";
|
|
@@ -42,6 +44,8 @@ export interface IntegrationStateStore {
|
|
|
42
44
|
putRecord(record: OwnershipRecord): void;
|
|
43
45
|
dropRecord(clientId: IntegrationClientId): void;
|
|
44
46
|
appendJournal(entry: JournalEntry): void;
|
|
47
|
+
/** Retire one operation. Append-only; see journal.ts `appendTombstone`. */
|
|
48
|
+
retireOperation(record: JournalTombstone): void;
|
|
45
49
|
listOperations(clientId?: IntegrationClientId, limit?: number): JournalEntry[];
|
|
46
50
|
findOperation(opId: string): JournalEntry | null;
|
|
47
51
|
captureSnapshot(clientId: IntegrationClientId, opId: string, text: string | null): SnapshotRef;
|
|
@@ -78,6 +82,7 @@ export function createIntegrationStateStore(root: string = integrationsDir()): I
|
|
|
78
82
|
putRecord: record => writeRecord(record, dir),
|
|
79
83
|
dropRecord: clientId => deleteRecord(clientId, dir),
|
|
80
84
|
appendJournal: entry => appendOperation(entry, dir),
|
|
85
|
+
retireOperation: record => appendTombstone(record, dir),
|
|
81
86
|
listOperations: (clientId, limit) => listOperations(clientId, limit, dir),
|
|
82
87
|
findOperation: opId => findOperation(opId, dir),
|
|
83
88
|
captureSnapshot: (clientId, opId, text) => captureSnapshot(clientId, opId, text, dir),
|
package/src/lab/events/limits.ts
CHANGED
|
@@ -33,6 +33,8 @@ const FORBIDDEN_EXACT_KEYS = new Set([
|
|
|
33
33
|
|
|
34
34
|
const RAW_POSIX_PATH_RE =
|
|
35
35
|
/(?:^|[^A-Za-z0-9._~/])\/(?:(?=$|[^A-Za-z0-9._~/])|(?!\/)(?![ \t\r\n])(?:\/|[^/\0\r\n]+)+\/?(?=$|[^A-Za-z0-9._~/]))/u;
|
|
36
|
+
const ASCII_URL_WHITESPACE_RE = /[\t\r\n]/g;
|
|
37
|
+
const FILE_URI_RE = /(?:^|[^A-Za-z0-9+.-])file:/i;
|
|
36
38
|
|
|
37
39
|
function fieldPath(base: string, key: string | number): string {
|
|
38
40
|
return base ? `${base}.${String(key)}` : String(key);
|
|
@@ -80,6 +82,8 @@ export function enforceEventStructureLimits(
|
|
|
80
82
|
}
|
|
81
83
|
if (
|
|
82
84
|
/^[A-Za-z]:\\/.test(value) ||
|
|
85
|
+
FILE_URI_RE.test(value) ||
|
|
86
|
+
FILE_URI_RE.test(value.replace(ASCII_URL_WHITESPACE_RE, "")) ||
|
|
83
87
|
RAW_POSIX_PATH_RE.test(value) ||
|
|
84
88
|
value.includes("\\Users\\")
|
|
85
89
|
) {
|
|
@@ -145,6 +145,25 @@ function isBenchmarkDnsAnswer(address: string, assessment: DestinationAssessment
|
|
|
145
145
|
return embedded.kind === "private" && embedded.detail === "benchmark address";
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Mihomo (Clash.Meta) fake-IP DNS answers IPv6 queries from `fdfe:dcba:9876::/48` — its
|
|
150
|
+
* documented default `fake-ip-range6` (#3462). That prefix sits inside ULA `fc00::/7`, so
|
|
151
|
+
* `classifyIpv6` reports it as a private-network address and, unlike the IPv4 benchmark
|
|
152
|
+
* range, nothing about the address itself marks it synthetic. The exception is therefore
|
|
153
|
+
* narrower than the benchmark one: exact /48 match, DNS answers only (a literal URL still
|
|
154
|
+
* rejects), and only behind the `allowMihomoIpv6FakeIp` opt-in that the outbound caller
|
|
155
|
+
* derives from a scheme-matched proxy it then binds the request to.
|
|
156
|
+
*/
|
|
157
|
+
const MIHOMO_IPV6_FAKE_IP_PREFIX = [0xfdfe, 0xdcba, 0x9876] as const;
|
|
158
|
+
|
|
159
|
+
function isMihomoIpv6FakeIpAnswer(address: string, assessment: DestinationAssessment | null): boolean {
|
|
160
|
+
if (assessment?.kind !== "private" || assessment.detail !== "private-network address") return false;
|
|
161
|
+
if (isIP(address) !== 6) return false;
|
|
162
|
+
const hextets = ipv6Hextets(normalizeHostname(address));
|
|
163
|
+
if (!hextets) return false;
|
|
164
|
+
return MIHOMO_IPV6_FAKE_IP_PREFIX.every((group, index) => hextets[index] === group);
|
|
165
|
+
}
|
|
166
|
+
|
|
148
167
|
function firstIpv6Hextet(hostname: string): number | null {
|
|
149
168
|
const head = hostname.split(":")[0];
|
|
150
169
|
if (!head) return 0;
|
|
@@ -385,7 +404,13 @@ export function assessUrlDestination(url: string): UrlDestinationAssessment | nu
|
|
|
385
404
|
*/
|
|
386
405
|
export async function resolvePublicAddresses(
|
|
387
406
|
url: string,
|
|
388
|
-
options?: string | {
|
|
407
|
+
options?: string | {
|
|
408
|
+
context?: string;
|
|
409
|
+
allowPrivateNetwork?: boolean;
|
|
410
|
+
allowBenchmarkAddresses?: boolean;
|
|
411
|
+
/** Mihomo IPv6 fake-IP (`fdfe:dcba:9876::/48`) DNS answers; see `isMihomoIpv6FakeIpAnswer`. */
|
|
412
|
+
allowMihomoIpv6FakeIp?: boolean;
|
|
413
|
+
},
|
|
389
414
|
): Promise<{
|
|
390
415
|
hostname: string;
|
|
391
416
|
addresses: { address: string; family: number }[];
|
|
@@ -396,6 +421,7 @@ export async function resolvePublicAddresses(
|
|
|
396
421
|
: options?.context?.trim() || "image URL";
|
|
397
422
|
const privateNetworkAllowed = typeof options === "object" && options?.allowPrivateNetwork === true;
|
|
398
423
|
const benchmarkAllowed = typeof options === "object" && options?.allowBenchmarkAddresses === true;
|
|
424
|
+
const mihomoIpv6Allowed = typeof options === "object" && options?.allowMihomoIpv6FakeIp === true;
|
|
399
425
|
let hostname: string;
|
|
400
426
|
try {
|
|
401
427
|
hostname = normalizeHostname(new URL(url.trim()).hostname);
|
|
@@ -440,7 +466,10 @@ export async function resolvePublicAddresses(
|
|
|
440
466
|
// fake-IP DNS, not a LAN provider. Accept it without allowPrivateNetwork and
|
|
441
467
|
// do not mark the destination private, so the caller's HTTP(S)_PROXY path
|
|
442
468
|
// still applies (credit #1748).
|
|
443
|
-
if (
|
|
469
|
+
if (
|
|
470
|
+
(benchmarkAllowed && isBenchmarkDnsAnswer(address, assessment))
|
|
471
|
+
|| (mihomoIpv6Allowed && isMihomoIpv6FakeIpAnswer(address, assessment))
|
|
472
|
+
) {
|
|
444
473
|
validatedAddresses.push({ address, family: ipKind === 4 || ipKind === 6 ? ipKind : (family || 4) });
|
|
445
474
|
continue;
|
|
446
475
|
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -127,6 +127,28 @@ function isPermissionMessage(text: string): boolean {
|
|
|
127
127
|
);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Geographic / network-location denials. Google's Cloud Code Assist API returns these as
|
|
132
|
+
* HTTP 400 `FAILED_PRECONDITION: User location is not supported for the API use.` — the
|
|
133
|
+
* request is well-formed, the caller's location is refused. Treated as a permission-class
|
|
134
|
+
* rejection, never as an invalid request (#3467).
|
|
135
|
+
*/
|
|
136
|
+
const LOCATION_UNSUPPORTED_PATTERNS = [
|
|
137
|
+
"location is not supported",
|
|
138
|
+
"location not supported",
|
|
139
|
+
"unsupported location",
|
|
140
|
+
"region is not supported",
|
|
141
|
+
"unsupported region",
|
|
142
|
+
"country is not supported",
|
|
143
|
+
"not supported in your country",
|
|
144
|
+
"not supported in your region",
|
|
145
|
+
] as const;
|
|
146
|
+
|
|
147
|
+
export function isLocationUnsupportedMessage(text: string): boolean {
|
|
148
|
+
const lower = text.toLowerCase();
|
|
149
|
+
return LOCATION_UNSUPPORTED_PATTERNS.some(needle => lower.includes(needle));
|
|
150
|
+
}
|
|
151
|
+
|
|
130
152
|
/**
|
|
131
153
|
* Client cancelled / closed the turn. Matches ONLY abort phrases this codebase
|
|
132
154
|
* produces — "client closed request during web-search" (src/web-search/loop.ts),
|
|
@@ -244,6 +266,15 @@ export function classifyError(status: number, type: string, message: string): Oc
|
|
|
244
266
|
) {
|
|
245
267
|
return { message, type: "authentication_error", code: "invalid_api_key" };
|
|
246
268
|
}
|
|
269
|
+
// An explicit permission enum must not acquire a more specific inferred reason.
|
|
270
|
+
if (type === "PERMISSION_DENIED" || text.includes("permission_denied")) {
|
|
271
|
+
return { message, type: "permission_error", code: "permission_denied" };
|
|
272
|
+
}
|
|
273
|
+
// Location denials outrank generic permission / subscription wording, but never an
|
|
274
|
+
// authoritative 5xx. Message-only adapter terminals arrive here with inferred 403.
|
|
275
|
+
if (status < 500 && (type === "location_not_supported" || isLocationUnsupportedMessage(text))) {
|
|
276
|
+
return { message, type: "permission_error", code: "location_not_supported" };
|
|
277
|
+
}
|
|
247
278
|
// Subscription labels are valid only in a known permission context.
|
|
248
279
|
if (
|
|
249
280
|
(status === 403 || type === "permission_error") &&
|
|
@@ -352,6 +383,9 @@ export function inferHttpStatusFromAdapterMessage(message: string): number {
|
|
|
352
383
|
// Strong authentication signals win when a message contains mixed auth and
|
|
353
384
|
// subscription/permission wording.
|
|
354
385
|
if (isAuthenticationMessage(lower)) return 401;
|
|
386
|
+
// A location denial is a permission-class rejection; keep it aligned with the
|
|
387
|
+
// `permission_error` envelope status so message-only and classified paths agree.
|
|
388
|
+
if (isLocationUnsupportedMessage(lower)) return 403;
|
|
355
389
|
if (isSubscriptionGateMessage(lower) || isPermissionMessage(lower)) return 403;
|
|
356
390
|
// Same precedence rule as classifyCursorError: an explicit gRPC FAILED_PRECONDITION is a
|
|
357
391
|
// structured, deterministic rejection, so it outranks the overload keywords that routinely
|
|
@@ -359,6 +393,11 @@ export function inferHttpStatusFromAdapterMessage(message: string): number {
|
|
|
359
393
|
// the message matched "unavailable" and returned a retryable 503, so clients kept retrying
|
|
360
394
|
// a rejection that can never succeed.
|
|
361
395
|
if (lower.includes("failed_precondition") || lower.includes("failed precondition")) return 400;
|
|
396
|
+
// Bytes the upstream itself produced and then mangled are a provider protocol failure, not a
|
|
397
|
+
// malformed client request. This must sit ahead of the generic "malformed" -> 400 branch so a
|
|
398
|
+
// combo can fail over instead of returning a terminal 4xx the caller cannot act on. Scoped to
|
|
399
|
+
// the "malformed upstream" phrase our adapters emit; plain "malformed" keeps its 400 verdict.
|
|
400
|
+
if (lower.includes("malformed upstream")) return 502;
|
|
362
401
|
if (
|
|
363
402
|
lower.includes("unavailable") ||
|
|
364
403
|
lower.includes("overloaded") ||
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
resolvePublicAddresses,
|
|
8
8
|
} from "./destination-policy";
|
|
9
9
|
import { pinnedHttpGet, pinnedHttpPost } from "./pinned-http";
|
|
10
|
-
import { outboundProxyConfigured } from "./proxy-env";
|
|
10
|
+
import { effectiveProxyFor, outboundProxyConfigured } from "./proxy-env";
|
|
11
11
|
import { publicProviderBaseUrl } from "./provider-url";
|
|
12
12
|
|
|
13
13
|
type ProviderGetInit = Omit<RequestInit, "body" | "method" | "redirect">;
|
|
@@ -19,6 +19,14 @@ export interface ProviderOutboundDependencies {
|
|
|
19
19
|
resolveAddresses?: typeof resolvePublicAddresses;
|
|
20
20
|
pinnedGet?: typeof pinnedHttpGet;
|
|
21
21
|
pinnedPost?: typeof pinnedHttpPost;
|
|
22
|
+
/**
|
|
23
|
+
* Canonical-URL proof for the transparent fake-IP exception (Clash TUN mode
|
|
24
|
+
* without proxy env). Injected so this transport core stays decoupled from
|
|
25
|
+
* the registry module; production compares the final request URL against the
|
|
26
|
+
* registry's own fixed discovery URL. Defaults to "not canonical" so a caller
|
|
27
|
+
* that forgets the seam fails closed, never open.
|
|
28
|
+
*/
|
|
29
|
+
isCanonicalUrl?: (name: string, url: string) => boolean;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
export class ProviderOutboundPolicyError extends Error {
|
|
@@ -33,6 +41,41 @@ function configuredProxyFor(): boolean {
|
|
|
33
41
|
return outboundProxyConfigured();
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Registry-owned fake-IP transparency exception (Clash/Surge/Mihomo TUN mode).
|
|
46
|
+
*
|
|
47
|
+
* Under TUN mode the packet path intercepts the fake-IP destination itself, so a
|
|
48
|
+
* canonical registry destination whose local DNS answers include Clash fake-IP
|
|
49
|
+
* space (198.18.0.0/15) is reachable by pin-connecting through the TUN — no
|
|
50
|
+
* outbound HTTP(S) proxy env is required. The exception is deliberately narrow:
|
|
51
|
+
*
|
|
52
|
+
* - hostname-only: a literal 198.18.x.x URL never reaches it (the literal gate
|
|
53
|
+
* in `resolvePublicAddresses` rejects before DNS answers are examined);
|
|
54
|
+
* - canonical-URL-only: `isCanonicalUrl` must prove the FINAL request URL is
|
|
55
|
+
* the registry's own fixed discovery URL for this provider (not merely that
|
|
56
|
+
* the provider NAME matches — OAuth/forward names match any baseUrl by
|
|
57
|
+
* design, and the bearer is pinned to the registry destination independently
|
|
58
|
+
* in `buildModelsRequest`). A retargeted row or a renamed custom row sends
|
|
59
|
+
* its credential to the registry URL anyway, so the proof must be on the URL
|
|
60
|
+
* actually fetched. The check is injected so the transport core stays
|
|
61
|
+
* decoupled from the registry module;
|
|
62
|
+
* - per-answer validation: benchmark and public answers may coexist. The exception
|
|
63
|
+
* does not admit loopback/RFC1918/link-local/metadata companions; those still
|
|
64
|
+
* follow the resolver's private-network policy. Benchmark admission leaves
|
|
65
|
+
* `privateNetwork` false; proxy/NO_PROXY semantics are unchanged.
|
|
66
|
+
*
|
|
67
|
+
* Image/Lab fetch never passes the underlying flag and is unaffected.
|
|
68
|
+
*/
|
|
69
|
+
function transparentFakeIpException(
|
|
70
|
+
url: string,
|
|
71
|
+
parsed: URL,
|
|
72
|
+
isCanonicalUrl: (name: string, url: string) => boolean,
|
|
73
|
+
name: string,
|
|
74
|
+
): boolean {
|
|
75
|
+
if (noProxyMatches(parsed)) return false;
|
|
76
|
+
return isCanonicalUrl(name, url);
|
|
77
|
+
}
|
|
78
|
+
|
|
36
79
|
function normalizeProxyHostname(hostname: string): string {
|
|
37
80
|
const normalized = hostname.trim().toLowerCase().replace(/\.+$/, "");
|
|
38
81
|
return normalized.startsWith("[") && normalized.endsWith("]")
|
|
@@ -139,9 +182,15 @@ async function providerOutboundRequest(
|
|
|
139
182
|
}
|
|
140
183
|
const parsed = postUrl ?? new URL(url);
|
|
141
184
|
const proxyConfigured = configuredProxyFor();
|
|
185
|
+
// Snapshot the scheme-matched proxy once, before the DNS await, so admission and transport
|
|
186
|
+
// below reason about the same value. `null` here means "no proxy fetch would actually use",
|
|
187
|
+
// even if some other proxy variable is set.
|
|
188
|
+
const effectiveProxy = effectiveProxyFor(parsed);
|
|
189
|
+
const allowMihomoIpv6FakeIp = effectiveProxy !== null && !noProxyMatches(parsed);
|
|
142
190
|
const resolveAddresses = dependencies.resolveAddresses ?? resolvePublicAddresses;
|
|
143
191
|
const pinnedGet = dependencies.pinnedGet ?? pinnedHttpGet;
|
|
144
192
|
const pinnedPost = dependencies.pinnedPost ?? pinnedHttpPost;
|
|
193
|
+
const isCanonicalUrl = dependencies.isCanonicalUrl ?? (() => false);
|
|
145
194
|
const allowPrivate = providerAllowsPrivateNetwork(name, provider);
|
|
146
195
|
let resolved: Awaited<ReturnType<typeof resolvePublicAddresses>>;
|
|
147
196
|
try {
|
|
@@ -154,7 +203,21 @@ async function providerOutboundRequest(
|
|
|
154
203
|
// destination or being pin-connected to the fake-IP (credit #1748). A NO_PROXY
|
|
155
204
|
// match is a direct route, so it keeps the benchmark answer rejected. Image/Lab
|
|
156
205
|
// fetch never passes this flag.
|
|
157
|
-
|
|
206
|
+
//
|
|
207
|
+
// TUN-mode transparency: with no proxy env, Clash/Surge/Mihomo TUN still
|
|
208
|
+
// intercepts the fake-IP destination itself, so the REGISTRY's own fixed
|
|
209
|
+
// discovery URL stays reachable by pin-connecting through the TUN. The
|
|
210
|
+
// proof is on the final request URL — not the provider name — because an
|
|
211
|
+
// OAuth/forward name matches any baseUrl by design while the bearer is
|
|
212
|
+
// pinned to the registry destination independently.
|
|
213
|
+
allowBenchmarkAddresses: (proxyConfigured && !noProxyMatches(parsed))
|
|
214
|
+
|| transparentFakeIpException(url, parsed, isCanonicalUrl, name),
|
|
215
|
+
// Mihomo IPv6 fake-IP (fdfe:dcba:9876::/48) answers are admitted on a stricter gate
|
|
216
|
+
// than the benchmark range: the proxy must be the one fetch will use for this URL's
|
|
217
|
+
// scheme, and the request below is then bound to it explicitly (#3462). A ULA answer
|
|
218
|
+
// is otherwise indistinguishable from a real private host, so proxy presence alone
|
|
219
|
+
// is not enough.
|
|
220
|
+
allowMihomoIpv6FakeIp,
|
|
158
221
|
});
|
|
159
222
|
} catch (error) {
|
|
160
223
|
const dnsResolutionFailed = error instanceof DestinationDnsResolutionError
|
|
@@ -169,7 +232,10 @@ async function providerOutboundRequest(
|
|
|
169
232
|
}
|
|
170
233
|
if (proxyConfigured && !resolved.privateNetwork) {
|
|
171
234
|
warnProxyBoundaryOnce();
|
|
172
|
-
|
|
235
|
+
// When the Mihomo exception could have admitted an answer, pin the transport to the
|
|
236
|
+
// proxy the admission assumed instead of letting fetch re-infer it from the environment.
|
|
237
|
+
const proxy = allowMihomoIpv6FakeIp ? effectiveProxy : undefined;
|
|
238
|
+
return globalThis.fetch(url, { ...init, method, redirect: "manual", ...(proxy ? { proxy } : {}) });
|
|
173
239
|
}
|
|
174
240
|
if (proxyConfigured && resolved.privateNetwork && !noProxyMatches(parsed)) {
|
|
175
241
|
const hostname = normalizeProxyHostname(parsed.hostname);
|
package/src/lib/proxy-env.ts
CHANGED
|
@@ -16,3 +16,25 @@ export function outboundProxyConfigured(
|
|
|
16
16
|
): boolean {
|
|
17
17
|
return OUTBOUND_PROXY_ENV_KEYS.some(key => proxyEnvPresent(key, env));
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The proxy URL that Bun's fetch will actually use for `url`, or null when none applies.
|
|
22
|
+
*
|
|
23
|
+
* Bun selects by scheme: `HTTPS_PROXY` for `https:` targets, `HTTP_PROXY` for `http:`.
|
|
24
|
+
* `ALL_PROXY` is deliberately not consulted here — fetch does not honour it, so a caller
|
|
25
|
+
* that needs "this request will ride the proxy" as a precondition must not count it.
|
|
26
|
+
* Presence of *some* proxy variable (`outboundProxyConfigured`) is not that guarantee.
|
|
27
|
+
*/
|
|
28
|
+
export function effectiveProxyFor(
|
|
29
|
+
url: URL,
|
|
30
|
+
env: ProxyEnvMap = process.env,
|
|
31
|
+
): string | null {
|
|
32
|
+
const key: ProxyEnvKey | null = url.protocol === "https:"
|
|
33
|
+
? "HTTPS_PROXY"
|
|
34
|
+
: url.protocol === "http:"
|
|
35
|
+
? "HTTP_PROXY"
|
|
36
|
+
: null;
|
|
37
|
+
if (!key) return null;
|
|
38
|
+
const value = env[key]?.trim() || env[key.toLowerCase()]?.trim();
|
|
39
|
+
return value ? value : null;
|
|
40
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Characters that render as a colon separator. Folded to `:` in the matching
|
|
3
|
+
* view so a look-alike cannot hide a header from the label pattern.
|
|
4
|
+
*/
|
|
5
|
+
const COLON_CONFUSABLES = new Set([
|
|
6
|
+
"\uFF1A", "\uFE55", "\uFE13", "\uA789", "\u02D0", "\u2236",
|
|
7
|
+
"\u205A", "\u0589", "\u1361", "\u16EC", "\u1803", "\u2982", "\u2AF6", "\uFE30",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Characters dropped from the matching view: anything with no visible width
|
|
12
|
+
* that could split a label into pieces the pattern no longer recognizes.
|
|
13
|
+
* `\p{Default_Ignorable_Code_Point}` is the systematic answer — it covers the
|
|
14
|
+
* zero-width set, the bidi isolates and marks, the Mongolian vowel separator,
|
|
15
|
+
* and the variation selectors in one property instead of a list that review
|
|
16
|
+
* keeps finding another member of. `\p{Cf}` and combining marks are folded too.
|
|
17
|
+
*/
|
|
18
|
+
const INVISIBLE_FORMAT = /[\p{Default_Ignorable_Code_Point}\p{Cf}\p{Mn}\p{Me}]/u;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* HTML named character references.
|
|
22
|
+
*
|
|
23
|
+
* A hand-picked list is a coverage promise nobody can keep — review found
|
|
24
|
+
* `ⅈ`, `ⅇ`, and `ⅆ` decoding to compatibility letters that
|
|
25
|
+
* NFKD already maps onto `i`, `e`, and `d`, and the WHATWG table holds roughly
|
|
26
|
+
* 2200 entries. Neither Bun nor Node exposes that table, and pulling in a
|
|
27
|
+
* dependency to spell a header name is the wrong trade for this path.
|
|
28
|
+
*
|
|
29
|
+
* So names are not resolved at all. A named reference sitting inside a
|
|
30
|
+
* credential label is folded to a single placeholder character of unknown
|
|
31
|
+
* identity, and the label alternation accepts that placeholder wherever a
|
|
32
|
+
* letter may appear. Every named entity is covered, present and future,
|
|
33
|
+
* without claiming to know what any of them mean.
|
|
34
|
+
*/
|
|
35
|
+
const NAMED_ENTITY_PLACEHOLDER = "\u0001";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The handful of named references that spell a SEPARATOR rather than a letter.
|
|
39
|
+
* These have to resolve exactly, because the placeholder stands in for a letter
|
|
40
|
+
* position and a separator is structure, not a character of the name.
|
|
41
|
+
*/
|
|
42
|
+
const SEPARATOR_ENTITIES = new Map<string, string>([
|
|
43
|
+
["colon", ":"], ["semi", ";"], ["equals", "="], ["quot", '"'], ["apos", "'"],
|
|
44
|
+
["lt", "<"], ["gt", ">"], ["amp", "&"], ["sol", "/"], ["lowbar", "_"],
|
|
45
|
+
["hyphen", "-"], ["dash", "-"], ["ndash", "-"], ["mdash", "-"], ["minus", "-"],
|
|
46
|
+
["period", "."], ["comma", ","], ["num", "#"], ["nbsp", " "],
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Latin look-alikes for the ASCII letters that appear in credential labels.
|
|
51
|
+
* Cyrillic `а`/`е`, Greek `ο`, fullwidth forms and the mathematical alphabets
|
|
52
|
+
* all render as the label to a human, so the matching view folds them back.
|
|
53
|
+
* NFKD handles the width/font variants; this table covers the cross-script
|
|
54
|
+
* homoglyphs NFKD deliberately leaves alone.
|
|
55
|
+
*/
|
|
56
|
+
const LETTER_CONFUSABLES = new Map<string, string>([
|
|
57
|
+
// Cyrillic
|
|
58
|
+
["\u0430", "a"], ["\u0435", "e"], ["\u043E", "o"], ["\u0440", "p"], ["\u0441", "c"],
|
|
59
|
+
["\u0445", "x"], ["\u0443", "y"], ["\u04BB", "h"], ["\u0455", "s"], ["\u0456", "i"],
|
|
60
|
+
["\u0458", "j"], ["\u043A", "k"], ["\u0442", "t"], ["\u0432", "b"], ["\u043C", "m"],
|
|
61
|
+
["\u043D", "h"], ["\u0501", "d"], ["\u0503", "g"], ["\u051B", "q"], ["\u051D", "w"],
|
|
62
|
+
["\u04CF", "l"], ["\u0261", "g"], ["\u04AB", "c"], ["\u04BD", "e"], ["\u0459", "k"],
|
|
63
|
+
// Greek
|
|
64
|
+
["\u03B1", "a"], ["\u03BF", "o"], ["\u03C1", "p"], ["\u03BD", "v"], ["\u03BA", "k"],
|
|
65
|
+
["\u03B5", "e"], ["\u03C4", "t"], ["\u03B9", "i"], ["\u03C5", "u"], ["\u03C7", "x"],
|
|
66
|
+
["\u03B7", "n"], ["\u03BC", "u"], ["\u03C3", "o"], ["\u03B2", "b"], ["\u03B3", "y"],
|
|
67
|
+
// Latin extended / other
|
|
68
|
+
["\u0131", "i"], ["\u0269", "i"], ["\u1D0F", "o"], ["\u0280", "r"], ["\u01BF", "p"],
|
|
69
|
+
["\u0578", "n"], ["\u057D", "u"], ["\u0585", "o"], ["\u0581", "g"], ["\u2044", "/"],
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build a folded copy plus an index map back to the original string, so the
|
|
74
|
+
* match runs on normalized text while the output keeps every byte the match did
|
|
75
|
+
* not cover.
|
|
76
|
+
*/
|
|
77
|
+
export function foldForMatching(value: string, decodeEscapes = true): { folded: string; map: number[] } {
|
|
78
|
+
let folded = "";
|
|
79
|
+
const map: number[] = [];
|
|
80
|
+
// Serialization escapes are ALIASES for the label, not decoration: a JSON
|
|
81
|
+
// `\u0069`, a percent-encoded `%69`, and an XML `i` all spell the same
|
|
82
|
+
// field name to whatever parses the body, while spelling something else to a
|
|
83
|
+
// literal matcher. Decode them into the matching view (one folded character
|
|
84
|
+
// per escape, with the whole escape mapped back to its start) so
|
|
85
|
+
// `author\u0069zation`, `author%69zation`, and `authorization` are the
|
|
86
|
+
// label they claim to be.
|
|
87
|
+
const decodeEscape = (at: number): { ch: string; width: number } | null => {
|
|
88
|
+
// JSON `\uXXXX`, INCLUDING a surrogate pair. Decoding the halves
|
|
89
|
+
// independently left `\uD835\uDD69` as two lone surrogates, so the
|
|
90
|
+
// mathematical letter they spell was never normalized as one code point.
|
|
91
|
+
const json = /^\\u([0-9a-fA-F]{4})/.exec(value.slice(at, at + 6));
|
|
92
|
+
if (json) {
|
|
93
|
+
const high = parseInt(json[1]!, 16);
|
|
94
|
+
if (high >= 0xd800 && high <= 0xdbff) {
|
|
95
|
+
const low = /^\\u([0-9a-fA-F]{4})/.exec(value.slice(at + 6, at + 12));
|
|
96
|
+
const lowCode = low ? parseInt(low[1]!, 16) : NaN;
|
|
97
|
+
if (lowCode >= 0xdc00 && lowCode <= 0xdfff) {
|
|
98
|
+
return { ch: String.fromCharCode(high, lowCode), width: 12 };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { ch: String.fromCharCode(high), width: 6 };
|
|
102
|
+
}
|
|
103
|
+
// Percent encoding is UTF-8: consecutive `%XX` bytes form ONE character.
|
|
104
|
+
// Decoding each byte on its own turned `%D0%B5` into two unrelated
|
|
105
|
+
// Latin-1 characters instead of the Cyrillic `е` the fold would have
|
|
106
|
+
// recognized.
|
|
107
|
+
const pct = /^(?:%[0-9a-fA-F]{2})+/.exec(value.slice(at, at + 24));
|
|
108
|
+
if (pct) {
|
|
109
|
+
try {
|
|
110
|
+
const decoded = decodeURIComponent(pct[0]);
|
|
111
|
+
if (decoded.length >= 1) {
|
|
112
|
+
// Consume only the bytes that produced the FIRST character, so the
|
|
113
|
+
// rest of the sequence is decoded on the next iteration.
|
|
114
|
+
const first = String.fromCodePoint(decoded.codePointAt(0)!);
|
|
115
|
+
const bytes = new TextEncoder().encode(first).length;
|
|
116
|
+
return { ch: first, width: bytes * 3 };
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
const single = parseInt(pct[0].slice(1, 3), 16);
|
|
120
|
+
return { ch: String.fromCharCode(single), width: 3 };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const xml = /^&#(x[0-9a-fA-F]{1,6}|[0-9]{1,7});/.exec(value.slice(at, at + 11));
|
|
124
|
+
if (xml) {
|
|
125
|
+
const raw = xml[1]!;
|
|
126
|
+
const code = raw[0] === "x" || raw[0] === "X"
|
|
127
|
+
? parseInt(raw.slice(1), 16)
|
|
128
|
+
: parseInt(raw, 10);
|
|
129
|
+
if (Number.isFinite(code) && code > 0 && code <= 0x10ffff) {
|
|
130
|
+
return { ch: String.fromCodePoint(code), width: xml[0].length };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// HTML named references. `:` and the other separator names are
|
|
134
|
+
// resolved exactly; anything else folds to the opaque placeholder so the
|
|
135
|
+
// label still matches without pretending to know the character.
|
|
136
|
+
const named = /^&([A-Za-z][A-Za-z0-9]{1,31});/.exec(value.slice(at, at + 34));
|
|
137
|
+
if (named) {
|
|
138
|
+
const separator = SEPARATOR_ENTITIES.get(named[1]!.toLowerCase());
|
|
139
|
+
return { ch: separator ?? NAMED_ENTITY_PLACEHOLDER, width: named[0].length };
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
};
|
|
143
|
+
// Iterate by CODE POINT, not UTF-16 code unit: a supplementary character
|
|
144
|
+
// (mathematical letters, variation selectors above the BMP) is two units, so
|
|
145
|
+
// a per-unit loop hands each half to the property tests separately and
|
|
146
|
+
// neither half matches anything. `𝕩-api-key` and a U+E0100 inside a label
|
|
147
|
+
// both walked straight past the fold that way.
|
|
148
|
+
let i = 0;
|
|
149
|
+
while (i < value.length) {
|
|
150
|
+
const escaped = decodeEscapes ? decodeEscape(i) : null;
|
|
151
|
+
const ch = escaped ? escaped.ch : String.fromCodePoint(value.codePointAt(i)!);
|
|
152
|
+
const width = escaped ? escaped.width : ch.length;
|
|
153
|
+
if (INVISIBLE_FORMAT.test(ch)) {
|
|
154
|
+
i += width;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const mapped = COLON_CONFUSABLES.has(ch)
|
|
158
|
+
? ":"
|
|
159
|
+
: LETTER_CONFUSABLES.get(ch.toLowerCase())
|
|
160
|
+
// NFKD collapses fullwidth, circled, and mathematical letter variants
|
|
161
|
+
// onto their ASCII base.
|
|
162
|
+
?? (ch.normalize("NFKD").length === 1 ? ch.normalize("NFKD") : ch);
|
|
163
|
+
// One folded unit per source code point keeps the offset map aligned; a
|
|
164
|
+
// multi-unit fold would desynchronize it, so those keep the original.
|
|
165
|
+
folded += mapped.length === 1 ? mapped : ch;
|
|
166
|
+
// One map entry per EMITTED UTF-16 unit. An escaped supplementary
|
|
167
|
+
// character emits two units, and giving it one entry desynchronized every
|
|
168
|
+
// later offset — the mask then landed mid-token and left part of the
|
|
169
|
+
// credential behind.
|
|
170
|
+
const emittedText = mapped.length === 1 ? mapped : ch;
|
|
171
|
+
for (let k = 0; k < emittedText.length; k += 1) map.push(i);
|
|
172
|
+
i += width;
|
|
173
|
+
}
|
|
174
|
+
map.push(value.length);
|
|
175
|
+
return { folded, map };
|
|
176
|
+
}
|