@bitkyc08/opencodex 2.83.0 → 2.86.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 +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -3
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/ollama-native.ts +29 -1
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +3 -0
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +56 -15
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +24 -0
- package/src/bridge.ts +1 -1
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/context-windows.ts +54 -1
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +26 -2
- package/src/cli/account.ts +41 -5
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +14 -2
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +60 -11
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +21 -4
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +15 -2
- package/src/cli/help.ts +2 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +26 -8
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +20 -7
- package/src/cli/restart-scope.ts +3 -1
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +59 -16
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +7 -1
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +6 -3
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-models.ts +16 -2
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +15 -6
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +18 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +56 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +8 -2
- package/src/config/paths.ts +13 -3
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +19 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +713 -285
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/reset-coupon-ledger.ts +75 -5
- package/src/grok/reset-coupons.ts +4 -2
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +14 -7
- package/src/integrations/registry.ts +52 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/store.ts +3 -0
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +24 -7
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/shadow-call.ts +44 -5
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +28 -17
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +55 -6
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +11 -0
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +21 -20
- package/src/providers/registry/entries-extended.ts +19 -1
- package/src/providers/registry/model-seeds.ts +17 -0
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +37 -20
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +46 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +15 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/lifecycle.ts +2 -1
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +50 -14
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/grok-coupon-routes.ts +78 -27
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +82 -402
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +16 -3
- package/src/server/management/shared.ts +3 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +9 -1
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +73 -43
- package/src/server/relay-eager.ts +2 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +63 -6
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +261 -81
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +313 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +126 -48
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +24 -1
- package/src/server/responses/core-combo.ts +28 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +48 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/passthrough-delivery.ts +20 -11
- package/src/server/responses/passthrough-dispatch.ts +29 -7
- package/src/server/responses/request-prepare.ts +74 -9
- package/src/server/responses/request-transport.ts +22 -8
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +89 -5
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +38 -4
- package/src/server/startup-health-cache.ts +5 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +161 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +466 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +71 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +68 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +19 -1
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-Ba8uG696.js +0 -54
- package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
- package/gui/dist/assets/index-BHRZdJuv.js +0 -94
- package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { UsageRelayController } from './usage-controller';
|
|
2
|
+
import { controlledUsageSse } from './usage-sse-controller';
|
|
3
|
+
import { boundedJsonBody } from './json-body';
|
|
4
|
+
|
|
5
|
+
/** Inject as the listener's fetchImpl. Existing auth forwarding/TLS/WS code stays unchanged. */
|
|
6
|
+
export function createUsageControlledFetch(controller: UsageRelayController, upstreamFetch: typeof fetch = fetch): typeof fetch {
|
|
7
|
+
return (async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
|
|
8
|
+
const upstream = await upstreamFetch(input, init);
|
|
9
|
+
const pathname = new URL(input instanceof Request ? input.url : String(input)).pathname;
|
|
10
|
+
const method = init?.method ?? (input instanceof Request ? input.method : 'GET');
|
|
11
|
+
const exchange = { pathname, method, status: upstream.status };
|
|
12
|
+
if (method !== 'GET' || upstream.status !== 200 || !upstream.body
|
|
13
|
+
|| !['/backend-api/wham/usage', '/backend-api/wham/usage/stream'].includes(pathname)) return upstream;
|
|
14
|
+
const contentType = upstream.headers.get('content-type') ?? '';
|
|
15
|
+
const headers = new Headers(upstream.headers);
|
|
16
|
+
const output = (body: BodyInit, transformed = false) => {
|
|
17
|
+
headers.delete('content-length'); headers.delete('content-encoding');
|
|
18
|
+
if (transformed) {
|
|
19
|
+
for (const name of ['etag', 'last-modified', 'digest', 'content-md5']) headers.delete(name);
|
|
20
|
+
headers.set('cache-control', 'no-store');
|
|
21
|
+
}
|
|
22
|
+
return new Response(body, { status: upstream.status, statusText: upstream.statusText, headers });
|
|
23
|
+
};
|
|
24
|
+
if (contentType.includes('application/json') || contentType.endsWith('+json')) {
|
|
25
|
+
const body = await boundedJsonBody(upstream.body);
|
|
26
|
+
if ('stream' in body) return output(body.stream);
|
|
27
|
+
let text: string;
|
|
28
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(body.bytes); }
|
|
29
|
+
catch { return output(new Uint8Array(body.bytes)); }
|
|
30
|
+
const changed = await controller.rewriteJson(text, exchange);
|
|
31
|
+
return output(changed ?? new Uint8Array(body.bytes), changed !== null);
|
|
32
|
+
}
|
|
33
|
+
if (!contentType.includes('text/event-stream') || pathname !== '/backend-api/wham/usage/stream') return upstream;
|
|
34
|
+
let registration: ReturnType<UsageRelayController['registerStream']> = null;
|
|
35
|
+
const reader = upstream.body.pipeThrough(controlledUsageSse(text => controller.rewriteJson(text, exchange, registration))).getReader();
|
|
36
|
+
let ended = false, sink: ReadableStreamDefaultController<Uint8Array>;
|
|
37
|
+
const close = async () => {
|
|
38
|
+
if (ended) return;
|
|
39
|
+
ended = true; registration?.release();
|
|
40
|
+
const cancelled = reader.cancel();
|
|
41
|
+
try { sink.close(); } catch { /* caller already cancelled */ }
|
|
42
|
+
await cancelled;
|
|
43
|
+
};
|
|
44
|
+
const body = new ReadableStream<Uint8Array>({
|
|
45
|
+
start(value) { sink = value; },
|
|
46
|
+
async pull(value) {
|
|
47
|
+
try {
|
|
48
|
+
const next = await reader.read();
|
|
49
|
+
if (ended) return;
|
|
50
|
+
if (next.done) { ended = true; registration?.release(); value.close(); }
|
|
51
|
+
else value.enqueue(next.value);
|
|
52
|
+
} catch (error) { if (!ended) { ended = true; registration?.release(); value.error(error); } }
|
|
53
|
+
},
|
|
54
|
+
cancel: close,
|
|
55
|
+
});
|
|
56
|
+
registration = controller.registerStream(exchange, close);
|
|
57
|
+
return output(body, true);
|
|
58
|
+
}) as typeof fetch;
|
|
59
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { UsageActivation, type ApplyScope } from './usage-activation';
|
|
2
|
+
import { UsageRefreshRegistry } from './usage-refresh';
|
|
3
|
+
import { evaluateUsageRewrite, type UsageRewriteContext } from './usage-policy';
|
|
4
|
+
|
|
5
|
+
export type UsageIdentity = NonNullable<UsageRewriteContext['account']> & { credentialGeneration?: string };
|
|
6
|
+
type Registration = NonNullable<ReturnType<UsageRefreshRegistry['register']>>;
|
|
7
|
+
type Exchange = { method: string; pathname: string; status: number };
|
|
8
|
+
const same = (a: UsageIdentity | null, b: UsageIdentity) => a !== null
|
|
9
|
+
&& a.id === b.id && a.userId === b.userId && a.plan === b.plan && a.structure === b.structure
|
|
10
|
+
&& a.credentialGeneration === b.credentialGeneration;
|
|
11
|
+
const object = (value: unknown): value is Record<string, unknown> => value !== null
|
|
12
|
+
&& typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
|
|
14
|
+
/** Response-level controller. Construction never starts a listener, timer or trust operation. */
|
|
15
|
+
export class UsageRelayController {
|
|
16
|
+
private readonly account: UsageIdentity;
|
|
17
|
+
private readonly key: string;
|
|
18
|
+
private readonly refresh = new UsageRefreshRegistry();
|
|
19
|
+
private readonly activation: UsageActivation;
|
|
20
|
+
private jsonSnapshots = 0;
|
|
21
|
+
private streamSnapshots = 0;
|
|
22
|
+
private lastSnapshotAt: number | null = null;
|
|
23
|
+
constructor(account: UsageIdentity, private readonly readCurrentIdentity: () => Promise<UsageIdentity | null>,
|
|
24
|
+
verifyFreshIdentity: () => Promise<UsageIdentity | null>, private readonly clock: () => number,
|
|
25
|
+
private readonly expiresAt: number, timeoutMs = 180000, private readonly contextValid: () => boolean | Promise<boolean> = () => true) {
|
|
26
|
+
if (!account.id || !account.userId || !['plus', 'pro'].includes(account.plan)
|
|
27
|
+
|| account.structure !== 'personal' || !Number.isFinite(expiresAt) || expiresAt <= clock()) {
|
|
28
|
+
throw new Error('A verified supported identity and finite safety deadline are required');
|
|
29
|
+
}
|
|
30
|
+
this.account = { ...account };
|
|
31
|
+
this.key = JSON.stringify([account.id, account.userId, account.plan, account.credentialGeneration ?? null]);
|
|
32
|
+
this.activation = new UsageActivation(this.key, key => this.refresh.refresh(key), async () => {
|
|
33
|
+
return same(await verifyFreshIdentity(), this.account) ? this.key : null;
|
|
34
|
+
}, clock, timeoutMs);
|
|
35
|
+
}
|
|
36
|
+
snapshot() { return { ...this.activation.snapshot(), trackedStreams: this.refresh.size, expired: this.clock() >= this.expiresAt,
|
|
37
|
+
observation: { jsonSnapshots: this.jsonSnapshots, streamSnapshots: this.streamSnapshots,
|
|
38
|
+
validatedActiveStreams: this.refresh.boundSize, lastSnapshotAt: this.lastSnapshotAt,
|
|
39
|
+
sourceProcessVerified: false as const, composerRecoveryVerified: false as const } }; }
|
|
40
|
+
private async checkContext() { try { return await this.contextValid(); } catch { return false; } }
|
|
41
|
+
async activate(options: { scope: ApplyScope; accountWideConsent: boolean }) {
|
|
42
|
+
const generation = this.activation.snapshot().generation, valid = await this.checkContext();
|
|
43
|
+
if (generation !== this.activation.snapshot().generation) return { accepted: false, reason: 'superseded', ...this.snapshot() };
|
|
44
|
+
if (!valid) { this.activation.invalidateIdentity(); return { accepted: false, reason: 'native-context-changed', ...this.snapshot() }; }
|
|
45
|
+
if (this.clock() >= this.expiresAt) { this.activation.invalidateIdentity(); return { accepted: false, reason: 'safety-deadline', ...this.snapshot() }; }
|
|
46
|
+
return this.activation.activate(options);
|
|
47
|
+
}
|
|
48
|
+
observeOnly() { return this.activation.observeOnly(); }
|
|
49
|
+
expireIfNeeded() { return this.activation.expireIfNeeded(); }
|
|
50
|
+
registerStream(exchange: Exchange, close: () => Promise<void>): Registration | null {
|
|
51
|
+
if (exchange.status !== 200) return null;
|
|
52
|
+
return this.refresh.register(exchange.method, exchange.pathname, close);
|
|
53
|
+
}
|
|
54
|
+
/** Only a complete original usage record may establish the stream's account binding. */
|
|
55
|
+
async rewriteJson(text: string, exchange: Exchange, stream?: Registration | null): Promise<string | null> {
|
|
56
|
+
if (exchange.method !== 'GET' || exchange.status !== 200
|
|
57
|
+
|| !['/backend-api/wham/usage', '/backend-api/wham/usage/stream'].includes(exchange.pathname)) return null;
|
|
58
|
+
if (this.clock() >= this.expiresAt) { this.activation.invalidateIdentity(); stream?.exclude(); return null; }
|
|
59
|
+
const observedGeneration = this.activation.snapshot().generation;
|
|
60
|
+
let current: UsageIdentity | null;
|
|
61
|
+
try { current = await this.readCurrentIdentity(); } catch { current = null; }
|
|
62
|
+
if (!same(current, this.account)) { this.activation.invalidateIdentity(); stream?.exclude(); return null; }
|
|
63
|
+
// Do not apply an activation that raced this response's identity read.
|
|
64
|
+
if (observedGeneration !== this.activation.snapshot().generation) return null;
|
|
65
|
+
if (this.clock() >= this.expiresAt) { this.activation.invalidateIdentity(); stream?.exclude(); return null; }
|
|
66
|
+
if (Buffer.byteLength(text, 'utf8') > 262144) { stream?.exclude(); return null; }
|
|
67
|
+
let parsed: unknown;
|
|
68
|
+
try { parsed = JSON.parse(text); } catch { stream?.exclude(); return null; }
|
|
69
|
+
const envelope = object(parsed) && 'usage' in parsed ? parsed : null;
|
|
70
|
+
if ((exchange.pathname.endsWith('/stream') && !envelope)
|
|
71
|
+
|| (envelope && (envelope.version !== 1 || typeof envelope.stream_id !== 'string' || !envelope.stream_id
|
|
72
|
+
|| !Number.isSafeInteger(envelope.sequence) || Number(envelope.sequence) <= 0))) { stream?.exclude(); return null; }
|
|
73
|
+
const original = envelope ? envelope.usage : parsed;
|
|
74
|
+
if (!object(original) || original.account_id !== this.account.id || original.user_id !== this.account.userId
|
|
75
|
+
|| original.plan_type !== this.account.plan) { stream?.exclude(); return null; }
|
|
76
|
+
const rate = original.rate_limit;
|
|
77
|
+
if (!object(rate) || typeof rate.allowed !== 'boolean' || typeof rate.limit_reached !== 'boolean') {
|
|
78
|
+
stream?.exclude(); return null;
|
|
79
|
+
}
|
|
80
|
+
if (stream && !stream.bind(this.key)) return null;
|
|
81
|
+
// Identity-checked responses received by this relay do not establish the
|
|
82
|
+
// sending process or prove that an authoritative UI cache consumed them.
|
|
83
|
+
if (exchange.pathname.endsWith('/stream')) this.streamSnapshots = Math.min(Number.MAX_SAFE_INTEGER, this.streamSnapshots + 1);
|
|
84
|
+
else this.jsonSnapshots = Math.min(Number.MAX_SAFE_INTEGER, this.jsonSnapshots + 1);
|
|
85
|
+
this.lastSnapshotAt = this.clock();
|
|
86
|
+
const context: UsageRewriteContext = { enabled: true, mode: 'observe', status: exchange.status,
|
|
87
|
+
pathname: exchange.pathname, account: this.account };
|
|
88
|
+
const observed = evaluateUsageRewrite(original, context);
|
|
89
|
+
this.activation.observe(this.key, observed.eligible ? 'exhausted'
|
|
90
|
+
: rate.allowed === true && rate.limit_reached === false ? 'available' : 'protected');
|
|
91
|
+
const state = this.activation.snapshot();
|
|
92
|
+
if (state.mode !== 'apply') return null;
|
|
93
|
+
const result = evaluateUsageRewrite(original, { ...context, mode: 'apply' });
|
|
94
|
+
if (!result.changed) return null;
|
|
95
|
+
// Observe and unchanged responses never spawn an installed-package probe.
|
|
96
|
+
const valid = await this.checkContext();
|
|
97
|
+
if (state.generation !== this.activation.snapshot().generation) return null;
|
|
98
|
+
if (!valid) { this.activation.invalidateIdentity(); stream?.exclude(); return null; }
|
|
99
|
+
// A native account or trial can change while the asynchronous build check runs.
|
|
100
|
+
try { current = await this.readCurrentIdentity(); } catch { current = null; }
|
|
101
|
+
if (state.generation !== this.activation.snapshot().generation) return null;
|
|
102
|
+
if (!same(current, this.account)) { this.activation.invalidateIdentity(); stream?.exclude(); return null; }
|
|
103
|
+
if (this.clock() >= this.expiresAt) { this.activation.invalidateIdentity(); stream?.exclude(); return null; }
|
|
104
|
+
if (!this.activation.recordOutput(this.key, state.generation)) return null;
|
|
105
|
+
return JSON.stringify(envelope ? { ...envelope, usage: result.value } : result.value);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Pure response policy. Real quota windows, credits and spending restrictions stay unchanged. */
|
|
2
|
+
export interface UsageRewriteContext {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
mode: 'observe' | 'apply';
|
|
5
|
+
status: number;
|
|
6
|
+
pathname: string;
|
|
7
|
+
account: {
|
|
8
|
+
id: string;
|
|
9
|
+
userId: string;
|
|
10
|
+
plan: 'plus' | 'pro';
|
|
11
|
+
structure: 'personal';
|
|
12
|
+
} | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type RecordValue = Record<string, unknown>;
|
|
16
|
+
function record(value: unknown): value is RecordValue {
|
|
17
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Transform only a complete, matched personal-account usage snapshot.
|
|
22
|
+
* Account context must come from a trusted live integration; this module does not
|
|
23
|
+
* discover it or prove provider isolation. Never persist the adjusted snapshot as
|
|
24
|
+
* actual provider/account capacity. Unknown or protected states pass unchanged.
|
|
25
|
+
*/
|
|
26
|
+
export function evaluateUsageRewrite(value: unknown, context: UsageRewriteContext) {
|
|
27
|
+
const unchanged = (reason: string) => ({ value, changed: false, eligible: false, reason });
|
|
28
|
+
if (!context.enabled) return unchanged('off');
|
|
29
|
+
if (context.status !== 200) return unchanged('non-success-response');
|
|
30
|
+
if (!['/backend-api/wham/usage', '/backend-api/wham/usage/stream'].includes(context.pathname)) return unchanged('unowned-path');
|
|
31
|
+
const account = context.account;
|
|
32
|
+
if (!account || !account.id || !account.userId || account.structure !== 'personal' || !['plus', 'pro'].includes(account.plan)) return unchanged('unsupported-account');
|
|
33
|
+
if (!record(value) || value.account_id !== account.id || value.user_id !== account.userId || value.plan_type !== account.plan) return unchanged('identity-mismatch');
|
|
34
|
+
const rate = value.rate_limit;
|
|
35
|
+
if (!record(rate) || rate.allowed !== false || rate.limit_reached !== true) return unchanged('not-explicitly-exhausted');
|
|
36
|
+
const spend = value.spend_control;
|
|
37
|
+
// Absence is unknown, not evidence that no protected spending restriction exists.
|
|
38
|
+
if (!record(spend) || spend.reached !== false) return unchanged('protected-or-unknown-spend');
|
|
39
|
+
const reason = value.rate_limit_reached_type;
|
|
40
|
+
if (reason != null && (!record(reason) || reason.type !== 'rate_limit_reached')) return unchanged('protected-or-unknown-reason');
|
|
41
|
+
const credits = value.credits;
|
|
42
|
+
if (!record(credits) || typeof credits.has_credits !== 'boolean' || typeof credits.unlimited !== 'boolean') return unchanged('unknown-credit-schema');
|
|
43
|
+
if (credits.overage_limit_reached != null && credits.overage_limit_reached !== false) return unchanged('overage-restriction');
|
|
44
|
+
// These entitlements already keep the native gate open. Rewriting the ordinary
|
|
45
|
+
// rate limit would unnecessarily change the app's Reserve activation predicate.
|
|
46
|
+
if (credits.has_credits || credits.unlimited) return unchanged('credits-available');
|
|
47
|
+
const upsell = value.rate_limit_upsell, additional = value.additional_rate_limits;
|
|
48
|
+
if (upsell != null && !record(upsell)) return unchanged('unknown-reserve-schema');
|
|
49
|
+
if (additional != null && (!Array.isArray(additional) || additional.some(limit => !record(limit)
|
|
50
|
+
|| limit.limit_name === 'gpt-reserve' && (!record(limit.rate_limit) || typeof limit.rate_limit.allowed !== 'boolean')))) return unchanged('unknown-reserve-schema');
|
|
51
|
+
if (record(upsell) && upsell.banner_type === 'luna_reserve' && Array.isArray(additional)
|
|
52
|
+
&& additional.some(limit => record(limit) && limit.limit_name === 'gpt-reserve'
|
|
53
|
+
&& record(limit.rate_limit) && limit.rate_limit.allowed === true)) return unchanged('reserve-active');
|
|
54
|
+
if (context.mode !== 'apply') return { value, changed: false, eligible: true, reason: 'observe-only' };
|
|
55
|
+
return {
|
|
56
|
+
value: { ...value, rate_limit: { ...rate, allowed: true, limit_reached: false } },
|
|
57
|
+
changed: true, eligible: true, reason: 'personal-usage-only',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Exact usage-stream refresh registry. No timers, sockets or core-path imports. */
|
|
2
|
+
export class UsageRefreshRegistry {
|
|
3
|
+
private entries = new Set<{ binding: string | null; tainted: boolean; close: () => Promise<void> }>();
|
|
4
|
+
register(method: string, pathname: string, close: () => Promise<void>) {
|
|
5
|
+
if (method !== 'GET' || pathname !== '/backend-api/wham/usage/stream') return null;
|
|
6
|
+
const entry = { binding: null as string | null, tainted: false, close };
|
|
7
|
+
this.entries.add(entry);
|
|
8
|
+
return {
|
|
9
|
+
// Caller must invoke only after validating a complete upstream snapshot's identity.
|
|
10
|
+
// A connection that changes account identity never becomes a refresh target again.
|
|
11
|
+
bind: (verifiedBinding: string) => {
|
|
12
|
+
if (!verifiedBinding || entry.tainted || !this.entries.has(entry)) return false;
|
|
13
|
+
if (entry.binding !== null && entry.binding !== verifiedBinding) {
|
|
14
|
+
entry.tainted = true; entry.binding = null; return false;
|
|
15
|
+
}
|
|
16
|
+
entry.binding = verifiedBinding; return true;
|
|
17
|
+
},
|
|
18
|
+
exclude: () => { entry.tainted = true; entry.binding = null; },
|
|
19
|
+
release: () => { this.entries.delete(entry); },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async refresh(binding: string) {
|
|
23
|
+
if (!binding) throw new Error('Verified account binding is required');
|
|
24
|
+
const selected = [...this.entries].filter(e => !e.tainted && e.binding === binding);
|
|
25
|
+
// Detach before awaiting; concurrent refreshes cannot close the same stream twice.
|
|
26
|
+
for (const entry of selected) this.entries.delete(entry);
|
|
27
|
+
const results = await Promise.allSettled(selected.map(entry => Promise.resolve().then(entry.close)));
|
|
28
|
+
return { requested: selected.length, closed: results.filter(r => r.status === 'fulfilled').length,
|
|
29
|
+
failed: results.filter(r => r.status === 'rejected').length };
|
|
30
|
+
}
|
|
31
|
+
get size() { return this.entries.size; }
|
|
32
|
+
get boundSize() { return [...this.entries].filter(entry => !entry.tainted && entry.binding !== null).length; }
|
|
33
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
type Rewrite = (text: string) => Promise<string | null>;
|
|
2
|
+
|
|
3
|
+
async function rewriteRecord(bytes: Buffer, first: boolean, rewrite: Rewrite): Promise<Buffer> {
|
|
4
|
+
let text: string;
|
|
5
|
+
try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes); }
|
|
6
|
+
catch { return Buffer.from(bytes); }
|
|
7
|
+
const lines = [...text.matchAll(/([^\r\n]*)(\r\n|\r|\n|$)/g)].filter(m => m[0].length > 0);
|
|
8
|
+
const data: string[] = [], indexes = new Set<number>(); let event = '';
|
|
9
|
+
lines.forEach((line, i) => {
|
|
10
|
+
const content = first && i === 0 ? line[1]!.replace(/^\uFEFF/, '') : line[1]!;
|
|
11
|
+
if (content.startsWith(':')) return;
|
|
12
|
+
const colon = content.indexOf(':');
|
|
13
|
+
const key = colon < 0 ? content : content.slice(0, colon);
|
|
14
|
+
const value = colon < 0 ? '' : content.slice(colon + 1).replace(/^ /, '');
|
|
15
|
+
if (key === 'event') event = value;
|
|
16
|
+
if (key === 'data') { data.push(value); indexes.add(i); }
|
|
17
|
+
});
|
|
18
|
+
if (event !== 'usage.snapshot' || !data.length) return Buffer.from(bytes);
|
|
19
|
+
const changed = await rewrite(data.join('\n'));
|
|
20
|
+
if (changed === null) return Buffer.from(bytes);
|
|
21
|
+
let written = false;
|
|
22
|
+
return Buffer.from(lines.map((line, i) => {
|
|
23
|
+
if (!indexes.has(i)) return line[0];
|
|
24
|
+
if (written) return '';
|
|
25
|
+
written = true;
|
|
26
|
+
const bom = first && i === 0 && line[1]!.startsWith('\uFEFF') ? '\uFEFF' : '';
|
|
27
|
+
return `${bom}data: ${changed}${line[2]}`;
|
|
28
|
+
}).join(''));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Byte-bounded SSE framing; never fabricates events or changes stream sequence numbers. */
|
|
32
|
+
export function controlledUsageSse(rewrite: Rewrite, cap = 262144): TransformStream<Uint8Array, Uint8Array> {
|
|
33
|
+
if (!Number.isSafeInteger(cap) || cap < 1 || cap > 1048576) throw new Error('Invalid record limit');
|
|
34
|
+
const buffer = Buffer.alloc(cap);
|
|
35
|
+
let used = 0, lineHasBytes = false, pendingCR = false, first = true, passthrough = false;
|
|
36
|
+
return new TransformStream({
|
|
37
|
+
async transform(chunk, controller) {
|
|
38
|
+
let rawStart = passthrough ? 0 : -1;
|
|
39
|
+
const enterPassthrough = (start: number) => {
|
|
40
|
+
controller.enqueue(Buffer.from(buffer.subarray(0, used)));
|
|
41
|
+
used = 0; passthrough = true; rawStart = start;
|
|
42
|
+
};
|
|
43
|
+
const endRecord = async (rawEnd: number) => {
|
|
44
|
+
if (passthrough) {
|
|
45
|
+
if (rawEnd > rawStart) controller.enqueue(chunk.subarray(rawStart, rawEnd));
|
|
46
|
+
passthrough = false; rawStart = -1;
|
|
47
|
+
} else {
|
|
48
|
+
controller.enqueue(await rewriteRecord(buffer.subarray(0, used), first, rewrite));
|
|
49
|
+
}
|
|
50
|
+
first = false; used = 0; lineHasBytes = false;
|
|
51
|
+
};
|
|
52
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
53
|
+
const byte = chunk[i]!;
|
|
54
|
+
if (pendingCR) {
|
|
55
|
+
pendingCR = false;
|
|
56
|
+
if (byte !== 10) {
|
|
57
|
+
if (!lineHasBytes) await endRecord(i);
|
|
58
|
+
lineHasBytes = false;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (!passthrough && used === cap) enterPassthrough(i);
|
|
62
|
+
if (!passthrough) buffer[used++] = byte;
|
|
63
|
+
const blankLine = !lineHasBytes;
|
|
64
|
+
lineHasBytes = false;
|
|
65
|
+
if (blankLine) await endRecord(i + 1);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!passthrough && used === cap) enterPassthrough(i);
|
|
70
|
+
if (!passthrough) buffer[used++] = byte;
|
|
71
|
+
if (byte === 13) pendingCR = true;
|
|
72
|
+
else if (byte === 10) {
|
|
73
|
+
const blankLine = !lineHasBytes;
|
|
74
|
+
lineHasBytes = false;
|
|
75
|
+
if (blankLine) await endRecord(i + 1);
|
|
76
|
+
} else lineHasBytes = true;
|
|
77
|
+
}
|
|
78
|
+
if (passthrough && rawStart < chunk.length) controller.enqueue(chunk.subarray(rawStart));
|
|
79
|
+
},
|
|
80
|
+
async flush(controller) {
|
|
81
|
+
if (passthrough) return;
|
|
82
|
+
if (pendingCR && !lineHasBytes) { controller.enqueue(await rewriteRecord(buffer.subarray(0, used), first, rewrite)); used = 0; }
|
|
83
|
+
if (used > 0) controller.enqueue(Buffer.from(buffer.subarray(0, used)));
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Kept inline so source and standalone Bun distributions carry the same COM adapter.
|
|
2
|
+
export const WINDOWS_ACTIVATION_SOURCE = String.raw`using System;
|
|
3
|
+
using System.Runtime.InteropServices;
|
|
4
|
+
using System.Text;
|
|
5
|
+
|
|
6
|
+
// Windows.Launch preserves package identity and forwards explicit application arguments.
|
|
7
|
+
public static class OpenCodexPackageActivation {
|
|
8
|
+
[ComImport, Guid("2E941141-7F97-4756-BA1D-9DECDE894A3D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
9
|
+
private interface IApplicationActivationManager {
|
|
10
|
+
[PreserveSig] int ActivateApplication([MarshalAs(UnmanagedType.LPWStr)] string appId, [MarshalAs(UnmanagedType.LPWStr)] string arguments, uint options, out uint processId);
|
|
11
|
+
[PreserveSig] int ActivateForFile([MarshalAs(UnmanagedType.LPWStr)] string appId, IntPtr items, [MarshalAs(UnmanagedType.LPWStr)] string verb, out uint processId);
|
|
12
|
+
[PreserveSig] int ActivateForProtocol([MarshalAs(UnmanagedType.LPWStr)] string appId, IntPtr items, out uint processId);
|
|
13
|
+
}
|
|
14
|
+
[DllImport("ole32.dll", PreserveSig=true)]
|
|
15
|
+
private static extern int CoCreateInstance(ref Guid clsid, IntPtr outer, uint context, ref Guid iid, [MarshalAs(UnmanagedType.Interface)] out IApplicationActivationManager manager);
|
|
16
|
+
[DllImport("kernel32.dll", SetLastError=true)] private static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
|
|
17
|
+
[DllImport("kernel32.dll")] private static extern bool CloseHandle(IntPtr handle);
|
|
18
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode)] private static extern int GetPackageFullName(IntPtr process, ref uint length, StringBuilder name);
|
|
19
|
+
private static IApplicationActivationManager Manager() {
|
|
20
|
+
var clsid=new Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C");
|
|
21
|
+
var iid=new Guid("2E941141-7F97-4756-BA1D-9DECDE894A3D");
|
|
22
|
+
IApplicationActivationManager manager;
|
|
23
|
+
// LOCAL_SERVER preserves activation argument lifetime after the short launcher exits.
|
|
24
|
+
Marshal.ThrowExceptionForHR(CoCreateInstance(ref clsid,IntPtr.Zero,4,ref iid,out manager));
|
|
25
|
+
return manager;
|
|
26
|
+
}
|
|
27
|
+
public static void ValidateActivationService() {var manager=Manager();Marshal.FinalReleaseComObject(manager);}
|
|
28
|
+
public static uint Activate(string appId,string arguments) {
|
|
29
|
+
var manager=Manager();
|
|
30
|
+
try {uint pid;Marshal.ThrowExceptionForHR(manager.ActivateApplication(appId,arguments,0,out pid));return pid;}
|
|
31
|
+
finally {Marshal.FinalReleaseComObject(manager);}
|
|
32
|
+
}
|
|
33
|
+
public static string PackageOf(uint pid) {
|
|
34
|
+
var handle=OpenProcess(0x1000,false,pid);
|
|
35
|
+
if(handle==IntPtr.Zero)throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
36
|
+
try {uint length=0;int status=GetPackageFullName(handle,ref length,null);
|
|
37
|
+
if(status==15700)return null;
|
|
38
|
+
if(status!=122)throw new System.ComponentModel.Win32Exception(status);
|
|
39
|
+
var value=new StringBuilder((int)length);status=GetPackageFullName(handle,ref length,value);
|
|
40
|
+
if(status!=0)throw new System.ComponentModel.Win32Exception(status);
|
|
41
|
+
return value.ToString();
|
|
42
|
+
}finally{CloseHandle(handle);}
|
|
43
|
+
}
|
|
44
|
+
}`;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { X509Certificate } from "node:crypto";
|
|
3
|
+
import { resolveTrustedWindowsPowerShellExe } from "../../lib/windows-elevation";
|
|
4
|
+
import type { StoredDesktopAuthority } from "./certificate-store";
|
|
5
|
+
import { isTestHomeGuardArmed } from "../../lib/test-home-guard";
|
|
6
|
+
import { isServerAuthOnlyCertificate } from "../../claude/intercept/local-ca";
|
|
7
|
+
|
|
8
|
+
export type DesktopCertificateTrust = "trusted" | "not-trusted" | "unknown";
|
|
9
|
+
export type TrustOperation = "inspect" | "trust" | "remove";
|
|
10
|
+
export type CertificateTrustRunner = (operation: TrustOperation, certificateDer: string, fingerprint: string) => Promise<DesktopCertificateTrust>;
|
|
11
|
+
|
|
12
|
+
const SCRIPT = [
|
|
13
|
+
"$ErrorActionPreference='Stop'",
|
|
14
|
+
"$inputData=[Console]::In.ReadToEnd()|ConvertFrom-Json",
|
|
15
|
+
"$bytes=[Convert]::FromBase64String($inputData.certificate)",
|
|
16
|
+
"$cert=[Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes)",
|
|
17
|
+
"$sha=[Security.Cryptography.SHA256]::Create()",
|
|
18
|
+
"try {$fingerprint=([BitConverter]::ToString($sha.ComputeHash($bytes))).Replace('-','')}finally{$sha.Dispose()}",
|
|
19
|
+
"if($fingerprint -cne $inputData.fingerprint){throw 'Certificate changed'}",
|
|
20
|
+
"$store=[Security.Cryptography.X509Certificates.X509Store]::new('Root','CurrentUser')",
|
|
21
|
+
"$mode=if($inputData.operation -eq 'inspect'){[Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly}else{[Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite}",
|
|
22
|
+
"try {",
|
|
23
|
+
" $store.Open($mode)",
|
|
24
|
+
" $foundCertificates=@($store.Certificates.Find([Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint,$cert.Thumbprint,$false))",
|
|
25
|
+
" foreach($certificateMatch in $foundCertificates){if([Convert]::ToBase64String($certificateMatch.RawData) -cne [Convert]::ToBase64String($bytes)){throw 'Certificate mismatch'}}",
|
|
26
|
+
" if($inputData.operation -eq 'trust'){if($foundCertificates.Count -eq 0){$store.Add($cert)}}",
|
|
27
|
+
" elseif($inputData.operation -eq 'remove'){foreach($certificateMatch in $foundCertificates){$store.Remove($certificateMatch)}}",
|
|
28
|
+
" elseif($inputData.operation -ne 'inspect'){throw 'Unknown operation'}",
|
|
29
|
+
" $present=@($store.Certificates.Find([Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint,$cert.Thumbprint,$false)).Count -gt 0",
|
|
30
|
+
" if($present){[Console]::Out.Write('trusted')}else{[Console]::Out.Write('not-trusted')}",
|
|
31
|
+
"}finally{$store.Close();$store.Dispose();$cert.Dispose()}",
|
|
32
|
+
].join("\n");
|
|
33
|
+
|
|
34
|
+
const run: CertificateTrustRunner = (operation, certificateDer, fingerprint) => {
|
|
35
|
+
if (process.platform !== "win32") return Promise.resolve("unknown");
|
|
36
|
+
if (operation !== "inspect" && isTestHomeGuardArmed()) return Promise.resolve("unknown");
|
|
37
|
+
return new Promise(resolve => {
|
|
38
|
+
const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
|
|
39
|
+
timeout: operation === "inspect" ? 10_000 : 120_000, maxBuffer: 4096, windowsHide: true, encoding: "utf8",
|
|
40
|
+
}, (error, stdout) => resolve(!error && (stdout.trim() === "trusted" || stdout.trim() === "not-trusted")
|
|
41
|
+
? stdout.trim() as DesktopCertificateTrust : "unknown"));
|
|
42
|
+
child.stdin?.on("error", () => { /* completion and readback decide the result */ });
|
|
43
|
+
child.stdin?.end(JSON.stringify({ operation, certificate: certificateDer, fingerprint }));
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Public status cannot create a certificate, decrypt a key or alter OS trust. */
|
|
48
|
+
export async function inspectWindowsCertificateTrust(certPem: string, expectedFingerprint: string, runner: CertificateTrustRunner = run): Promise<DesktopCertificateTrust> {
|
|
49
|
+
const certificate = new X509Certificate(certPem);
|
|
50
|
+
const fingerprint = certificate.fingerprint256.replaceAll(":", "");
|
|
51
|
+
if (!certificate.ca || fingerprint !== expectedFingerprint || !/^[A-F0-9]{64}$/.test(expectedFingerprint)) {
|
|
52
|
+
throw new Error("desktop_compatibility_certificate_mismatch");
|
|
53
|
+
}
|
|
54
|
+
return runner("inspect", certificate.raw.toString("base64"), fingerprint).catch(() => "unknown" as const);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Mutations require the store-validated private key, not unverified public status metadata. */
|
|
58
|
+
export function createWindowsCertificateTrust(authority: StoredDesktopAuthority, expectedFingerprint: string, runner: CertificateTrustRunner = run) {
|
|
59
|
+
const certificate = new X509Certificate(authority.authority.certPem);
|
|
60
|
+
const fingerprint = certificate.fingerprint256.replaceAll(":", "");
|
|
61
|
+
if (fingerprint !== expectedFingerprint || !certificate.ca || !certificate.checkPrivateKey(authority.authority.privateKey)
|
|
62
|
+
|| !certificate.verify(authority.authority.publicKey) || !/^OpenCodex Codex Desktop [0-9a-f-]{36}$/.test(authority.commonName)
|
|
63
|
+
|| !certificate.subject.split("\n").includes(`CN=${authority.commonName}`)) throw new Error("desktop_compatibility_certificate_mismatch");
|
|
64
|
+
const encoded = certificate.raw.toString("base64");
|
|
65
|
+
const inspect = () => runner("inspect", encoded, fingerprint).catch(() => "unknown" as const);
|
|
66
|
+
let pending: Promise<DesktopCertificateTrust> | null = null;
|
|
67
|
+
const change = (operation: "trust" | "remove") => {
|
|
68
|
+
if (pending) return Promise.reject(new Error("desktop_compatibility_trust_busy"));
|
|
69
|
+
if (operation === "trust" && Date.parse(certificate.validTo) <= Date.now()) return Promise.reject(new Error("desktop_compatibility_authority_expired"));
|
|
70
|
+
if (operation === "trust" && !isServerAuthOnlyCertificate(certificate)) return Promise.reject(new Error("desktop_compatibility_authority_renewal_required"));
|
|
71
|
+
pending = (async () => {
|
|
72
|
+
const before = await inspect();
|
|
73
|
+
if (before === "unknown") return before;
|
|
74
|
+
if (before === (operation === "trust" ? "trusted" : "not-trusted")) return before;
|
|
75
|
+
try { await runner(operation, encoded, fingerprint); } catch { /* read the exact store after an uncertain action */ }
|
|
76
|
+
return inspect();
|
|
77
|
+
})().finally(() => { pending = null; });
|
|
78
|
+
return pending;
|
|
79
|
+
};
|
|
80
|
+
return { inspect, trust: () => change("trust"), remove: () => change("remove") };
|
|
81
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { resolveTrustedWindowsPowerShellExe } from "../../lib/windows-elevation";
|
|
3
|
+
|
|
4
|
+
const MAX_INPUT = 65_536;
|
|
5
|
+
const MAX_OUTPUT = 131_072;
|
|
6
|
+
const PURPOSE = "opencodex/codex-desktop-compatibility/authority/v1";
|
|
7
|
+
// A cold Windows PowerShell that must also load System.Security has been measured past 10s on a
|
|
8
|
+
// loaded machine (Windows CI run 38591626343). The call is async and off the request path, so a
|
|
9
|
+
// wider bound costs nothing while healthy and turns a slow start into success instead of a refusal.
|
|
10
|
+
const DPAPI_TIMEOUT_MS = 20_000;
|
|
11
|
+
const SCRIPT = [
|
|
12
|
+
"$ErrorActionPreference='Stop'",
|
|
13
|
+
"Add-Type -AssemblyName System.Security",
|
|
14
|
+
"$r=[Console]::In.ReadToEnd()|ConvertFrom-Json",
|
|
15
|
+
"$data=[Convert]::FromBase64String($r.data)",
|
|
16
|
+
`if($data.Length -eq 0 -or $data.Length -gt ${MAX_INPUT}){throw 'Invalid data size'}`,
|
|
17
|
+
`$entropy=[Text.Encoding]::UTF8.GetBytes('${PURPOSE}')`,
|
|
18
|
+
"$scope=[Security.Cryptography.DataProtectionScope]::CurrentUser",
|
|
19
|
+
"if($r.operation -eq 'protect'){",
|
|
20
|
+
" $out=[Security.Cryptography.ProtectedData]::Protect($data,$entropy,$scope)",
|
|
21
|
+
"}elseif($r.operation -eq 'unprotect'){",
|
|
22
|
+
" $out=[Security.Cryptography.ProtectedData]::Unprotect($data,$entropy,$scope)",
|
|
23
|
+
"}else{throw 'Invalid operation'}",
|
|
24
|
+
"[Console]::Out.Write([Convert]::ToBase64String($out))",
|
|
25
|
+
].join("\n");
|
|
26
|
+
|
|
27
|
+
export interface AuthorityKeyProtection {
|
|
28
|
+
protect(cleartext: Uint8Array): Promise<Uint8Array>;
|
|
29
|
+
unprotect(ciphertext: Uint8Array): Promise<Uint8Array>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The payload travels on stdin, never in process arguments, environment or logs. */
|
|
33
|
+
function runDpapi(operation: "protect" | "unprotect", data: Uint8Array): Promise<Uint8Array> {
|
|
34
|
+
if (process.platform !== "win32") return Promise.reject(new Error("desktop_compatibility_windows_required"));
|
|
35
|
+
if (data.byteLength === 0 || data.byteLength > MAX_INPUT) return Promise.reject(new Error("desktop_compatibility_key_size"));
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
|
|
38
|
+
timeout: DPAPI_TIMEOUT_MS, maxBuffer: MAX_OUTPUT, windowsHide: true, encoding: "utf8",
|
|
39
|
+
}, (error, stdout) => {
|
|
40
|
+
// Do not include PowerShell stderr, command output or the input in a propagated error.
|
|
41
|
+
if (error || !/^[A-Za-z0-9+/]+={0,2}$/.test(stdout.trim())) {
|
|
42
|
+
reject(new Error("desktop_compatibility_key_protection_failed")); return;
|
|
43
|
+
}
|
|
44
|
+
const bytes = Buffer.from(stdout.trim(), "base64");
|
|
45
|
+
if (bytes.length === 0 || bytes.length > MAX_INPUT) {
|
|
46
|
+
reject(new Error("desktop_compatibility_key_size")); return;
|
|
47
|
+
}
|
|
48
|
+
resolve(bytes);
|
|
49
|
+
});
|
|
50
|
+
child.stdin?.on("error", () => { /* execFile's completion reports the failed child. */ });
|
|
51
|
+
child.stdin?.end(JSON.stringify({ operation, data: Buffer.from(data).toString("base64") }));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** CurrentUser protects against other OS identities, not other processes of the same user. */
|
|
56
|
+
export const windowsAuthorityKeyProtection: AuthorityKeyProtection = {
|
|
57
|
+
protect: bytes => runDpapi("protect", bytes),
|
|
58
|
+
unprotect: bytes => runDpapi("unprotect", bytes),
|
|
59
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { DesktopAppInstall, DesktopExec, DesktopProcess } from "../desktop-app/types";
|
|
2
|
+
import { resolveTrustedWindowsPowerShellExe } from "../../lib/windows-elevation";
|
|
3
|
+
import { WINDOWS_ACTIVATION_SOURCE } from "./windows-activation-source";
|
|
4
|
+
import { readDesktopCompatibilityLaunch } from "./runtime-ownership";
|
|
5
|
+
|
|
6
|
+
/** Only the feature's loopback PAC endpoint may become a launch argument. */
|
|
7
|
+
export function validatedCompatibilityPacUrl(value: string): string {
|
|
8
|
+
let url: URL;
|
|
9
|
+
try { url = new URL(value); } catch { throw new Error("desktop_compatibility_invalid_pac_url"); }
|
|
10
|
+
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !url.port || Number(url.port) < 1
|
|
11
|
+
|| url.username || url.password || url.search || url.hash
|
|
12
|
+
|| !/^\/[a-zA-Z0-9-]{16,80}\/proxy\.pac$/.test(url.pathname)
|
|
13
|
+
|| value !== url.href) throw new Error("desktop_compatibility_invalid_pac_url");
|
|
14
|
+
return url.href;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function literal(value: string): string { return `'${value.replaceAll("'", "''")}'`; }
|
|
18
|
+
|
|
19
|
+
export function compatibilityActivationScript(install: DesktopAppInstall, pacUrl: string): string {
|
|
20
|
+
const pac = validatedCompatibilityPacUrl(pacUrl);
|
|
21
|
+
if (!/^OpenAI\.Codex(?:Beta)?_[A-Za-z0-9]+$/.test(install.id) || install.relaunch !== `${install.id}!App`) {
|
|
22
|
+
throw new Error("desktop_compatibility_invalid_package");
|
|
23
|
+
}
|
|
24
|
+
return [
|
|
25
|
+
"$ErrorActionPreference='Stop'",
|
|
26
|
+
`$family=${literal(install.id)}; $root=${literal(install.root)}; $pac=${literal(pac)}`,
|
|
27
|
+
`$p=Get-AppxPackage -Name ${literal(install.id.split("_")[0]!)}`,
|
|
28
|
+
"$p=@($p|Where-Object {$_.PackageFamilyName -eq $family -and $_.InstallLocation -ieq $root})",
|
|
29
|
+
"if($p.Count -ne 1){throw 'Package changed'}; $p=$p[0]",
|
|
30
|
+
"$manifest=Get-AppxPackageManifest -Package $p.PackageFullName",
|
|
31
|
+
"$entry=@($manifest.Package.Applications.Application|Where-Object {$_.Id -eq 'App' -and $_.Executable.Replace('/','\\') -ieq 'app\\ChatGPT.exe'})",
|
|
32
|
+
"if($entry.Count -ne 1){throw 'Application entry changed'}",
|
|
33
|
+
"Add-Type -TypeDefinition @'", WINDOWS_ACTIVATION_SOURCE, "'@",
|
|
34
|
+
"$pidValue=[OpenCodexPackageActivation]::Activate($family+'!App','--proxy-pac-url='+$pac)",
|
|
35
|
+
"if([OpenCodexPackageActivation]::PackageOf($pidValue) -ne $p.PackageFullName){throw 'Package identity missing'}",
|
|
36
|
+
"$running=Get-CimInstance Win32_Process -Filter ('ProcessId='+$pidValue)",
|
|
37
|
+
"$expected=Join-Path $p.InstallLocation 'app\\ChatGPT.exe'",
|
|
38
|
+
"if(-not $running -or $running.ExecutablePath -ine $expected -or $running.CommandLine -notmatch ('(?:^|[\\s\"])--proxy-pac-url='+[regex]::Escape($pac)+'(?:$|[\\s\"])')){throw 'Launch arguments not observed'}",
|
|
39
|
+
"[pscustomobject]@{pid=[int]$pidValue;packageFullName=$p.PackageFullName;verified=$true}|ConvertTo-Json -Compress",
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/** Preserve only the current runtime's PAC and lifetime, never trust a URL shape alone. */
|
|
45
|
+
export function captureWindowsCompatibilityContext(processes: readonly DesktopProcess[]): Record<string, string> {
|
|
46
|
+
const members = new Set(processes.map(entry => entry.pid));
|
|
47
|
+
const urls = new Set<string>();
|
|
48
|
+
for (const entry of processes.filter(value => !members.has(value.parentPid))) {
|
|
49
|
+
if (entry.commandLine !== undefined && entry.commandLine.trim() === "") {
|
|
50
|
+
throw new Error("desktop_compatibility_launch_context_unavailable");
|
|
51
|
+
}
|
|
52
|
+
for (const match of (entry.commandLine ?? "").matchAll(/(?:^|\s)"?--proxy-pac-url=([^"\s]+)"?/g)) {
|
|
53
|
+
const url = match[1]!;
|
|
54
|
+
if (url.startsWith("http://127.0.0.1:")) urls.add(validatedCompatibilityPacUrl(url));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (urls.size > 1) throw new Error("desktop_compatibility_conflicting_launch_context");
|
|
58
|
+
if (urls.size === 0) return {};
|
|
59
|
+
const pacUrl = [...urls][0]!, owner = readDesktopCompatibilityLaunch();
|
|
60
|
+
if (!owner || owner.pacUrl !== pacUrl) throw new Error("desktop_compatibility_launch_owner_unverified");
|
|
61
|
+
return { codexCompatibilityPacUrl: pacUrl, codexCompatibilityGeneration: owner.generation };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Recheck after the asynchronous stop ladder: identical endpoints can belong to a new runtime. */
|
|
65
|
+
export function assertWindowsCompatibilityContext(context: Record<string, string>): void {
|
|
66
|
+
const owner = readDesktopCompatibilityLaunch();
|
|
67
|
+
if (!owner || owner.pacUrl !== context.codexCompatibilityPacUrl || owner.generation !== context.codexCompatibilityGeneration) {
|
|
68
|
+
throw new Error("desktop_compatibility_launch_owner_unverified");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function activateWindowsCodexCompatibility(exec: DesktopExec, install: DesktopAppInstall, pacUrl: string): { pid: number; packageFullName: string } {
|
|
73
|
+
const script = compatibilityActivationScript(install, pacUrl);
|
|
74
|
+
let text: string;
|
|
75
|
+
try { text = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: 15_000, windowsHide: true }); }
|
|
76
|
+
catch { throw new Error("desktop_compatibility_activation_unverified"); }
|
|
77
|
+
let result: unknown;
|
|
78
|
+
try { result = JSON.parse(text.trim().split(/\r?\n/).at(-1) ?? ""); }
|
|
79
|
+
catch { throw new Error("desktop_compatibility_activation_unverified"); }
|
|
80
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) throw new Error("desktop_compatibility_activation_unverified");
|
|
81
|
+
const value = result as Record<string, unknown>;
|
|
82
|
+
if (value.verified !== true || !Number.isSafeInteger(value.pid) || Number(value.pid) <= 0
|
|
83
|
+
|| typeof value.packageFullName !== "string" || !value.packageFullName.startsWith(install.id.split("_")[0]! + "_")
|
|
84
|
+
|| !value.packageFullName.endsWith("_" + install.id.split("_")[1]!)) throw new Error("desktop_compatibility_activation_unverified");
|
|
85
|
+
return { pid: Number(value.pid), packageFullName: value.packageFullName };
|
|
86
|
+
}
|