@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,62 @@
|
|
|
1
|
+
import { readFileSync, statSync, type BigIntStats } from 'node:fs';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import type { UsageIdentity } from './usage-controller';
|
|
4
|
+
|
|
5
|
+
type Credential = { identity: UsageIdentity; accessToken: string };
|
|
6
|
+
const equal = (a: UsageIdentity, b: UsageIdentity) => a.id === b.id && a.userId === b.userId && a.plan === b.plan
|
|
7
|
+
&& a.credentialGeneration === b.credentialGeneration;
|
|
8
|
+
|
|
9
|
+
/** Local native credentials remain in memory and are never returned by the public reader. */
|
|
10
|
+
export function createNativeIdentityReader(authPath: string, upstreamFetch: typeof fetch = fetch) {
|
|
11
|
+
const readerId = randomUUID(); let generation = 0, previousSnapshot: string | undefined;
|
|
12
|
+
const stamp = (value: BigIntStats) =>
|
|
13
|
+
`${value.dev}:${value.ino}:${value.size}:${value.mtimeNs}:${value.ctimeNs}`;
|
|
14
|
+
const readCredential = (): Credential => {
|
|
15
|
+
const before = statSync(authPath, { bigint: true });
|
|
16
|
+
if (before.size > 1048576n) throw new Error('Unexpected native auth size');
|
|
17
|
+
const text = readFileSync(authPath, 'utf8'), after = statSync(authPath, { bigint: true });
|
|
18
|
+
if (stamp(before) !== stamp(after)) throw new Error('Native credentials changed during read');
|
|
19
|
+
const snapshot = `${stamp(after)}:${createHash('sha256').update(text).digest('hex')}`;
|
|
20
|
+
if (snapshot !== previousSnapshot) { previousSnapshot = snapshot; generation++; }
|
|
21
|
+
const auth = JSON.parse(text);
|
|
22
|
+
if (auth.auth_mode !== 'chatgpt' || typeof auth.tokens?.id_token !== 'string'
|
|
23
|
+
|| typeof auth.tokens?.access_token !== 'string' || !auth.tokens.access_token
|
|
24
|
+
|| typeof auth.tokens?.account_id !== 'string') throw new Error('Native login required');
|
|
25
|
+
// These local claims are only a binding hint. Activation additionally verifies upstream.
|
|
26
|
+
const claims = JSON.parse(Buffer.from(auth.tokens.id_token.split('.')[1], 'base64url').toString('utf8'))['https://api.openai.com/auth'];
|
|
27
|
+
if (!claims || claims.chatgpt_account_id !== auth.tokens.account_id
|
|
28
|
+
|| typeof claims.chatgpt_user_id !== 'string' || !claims.chatgpt_user_id
|
|
29
|
+
|| !['plus', 'pro'].includes(claims.chatgpt_plan_type)) throw new Error('Unsupported identity');
|
|
30
|
+
return { identity: { id: auth.tokens.account_id, userId: claims.chatgpt_user_id,
|
|
31
|
+
plan: claims.chatgpt_plan_type, structure: 'personal', credentialGeneration: `${readerId}:${generation}` }, accessToken: auth.tokens.access_token };
|
|
32
|
+
};
|
|
33
|
+
const readCurrentIdentity = async (): Promise<UsageIdentity | null> => {
|
|
34
|
+
try { return readCredential().identity; } catch { return null; }
|
|
35
|
+
};
|
|
36
|
+
const verifyFreshIdentity = async (): Promise<UsageIdentity | null> => {
|
|
37
|
+
try {
|
|
38
|
+
const before = readCredential();
|
|
39
|
+
const response = await upstreamFetch('https://chatgpt.com/backend-api/wham/usage', {
|
|
40
|
+
headers: { authorization: 'Bearer ' + before.accessToken, 'ChatGPT-Account-ID': before.identity.id },
|
|
41
|
+
redirect: 'manual', signal: AbortSignal.timeout(10000),
|
|
42
|
+
});
|
|
43
|
+
if (response.status !== 200 || !response.body) { await response.body?.cancel(); return null; }
|
|
44
|
+
const reader = response.body.getReader(); const chunks: Uint8Array[] = []; let size = 0;
|
|
45
|
+
try {
|
|
46
|
+
for (;;) {
|
|
47
|
+
const part = await reader.read(); if (part.done) break;
|
|
48
|
+
size += part.value.length;
|
|
49
|
+
if (size > 65536) { await reader.cancel(); return null; }
|
|
50
|
+
chunks.push(part.value);
|
|
51
|
+
}
|
|
52
|
+
} finally { reader.releaseLock(); }
|
|
53
|
+
const usage = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
54
|
+
const after = readCredential();
|
|
55
|
+
if (!equal(before.identity, after.identity) || before.accessToken !== after.accessToken
|
|
56
|
+
|| usage.account_id !== before.identity.id || usage.user_id !== before.identity.userId
|
|
57
|
+
|| usage.plan_type !== before.identity.plan) return null;
|
|
58
|
+
return before.identity;
|
|
59
|
+
} catch { return null; }
|
|
60
|
+
};
|
|
61
|
+
return { readCurrentIdentity, verifyFreshIdentity };
|
|
62
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createServer } from "node:https";
|
|
2
|
+
import { Readable, type Duplex } from "node:stream";
|
|
3
|
+
import type { PemKeyPair } from "../../claude/intercept/local-ca";
|
|
4
|
+
import { forwardHeadersForUpstream } from "../../claude/intercept/listener";
|
|
5
|
+
import { dialDesktopUpstream, type DesktopTunnelOptions } from "../../lib/desktop-upstream-tunnel";
|
|
6
|
+
|
|
7
|
+
const ORIGIN = "https://chatgpt.com";
|
|
8
|
+
const STRIP = new Set(["connection", "keep-alive", "transfer-encoding", "content-encoding", "content-length", "alt-svc", "proxy-authenticate"]);
|
|
9
|
+
export interface DesktopRelayOptions {
|
|
10
|
+
leaf: PemKeyPair;
|
|
11
|
+
fetchImpl: typeof fetch;
|
|
12
|
+
/** Test-only TLS peer; the production destination is fixed, never taken from a request. */
|
|
13
|
+
websocketPeer?: { host: string; port: number; ca: string };
|
|
14
|
+
websocketTransport?: Pick<DesktopTunnelOptions, "proxy" | "proxyCa" | "timeoutMs">;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Transparent HTTP and raw upgraded-socket relay. Only fetchImpl owns usage policy. */
|
|
18
|
+
export async function startDesktopRelay(options: DesktopRelayOptions) {
|
|
19
|
+
const sockets = new Set<Duplex>(), requests = new Set<AbortController>();
|
|
20
|
+
const server = createServer({ cert: options.leaf.certPem, key: options.leaf.keyPem, ALPNProtocols: ["http/1.1"] });
|
|
21
|
+
server.on("connection", socket => { sockets.add(socket); socket.once("close", () => sockets.delete(socket)); });
|
|
22
|
+
const valid = (host: string | undefined, path: string | undefined) =>
|
|
23
|
+
(host?.toLowerCase() === "chatgpt.com" || host?.toLowerCase() === "chatgpt.com:443") && !!path && path.startsWith("/") && !path.startsWith("//");
|
|
24
|
+
server.on("request", (req, res) => {
|
|
25
|
+
if (!valid(req.headers.host, req.url)) { res.writeHead(421); res.end(); return; }
|
|
26
|
+
const abort = new AbortController(); requests.add(abort);
|
|
27
|
+
const finish = () => { requests.delete(abort); abort.abort(); };
|
|
28
|
+
res.once("close", finish); req.once("error", finish);
|
|
29
|
+
void (async () => {
|
|
30
|
+
const headers = new Headers();
|
|
31
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) headers.append(req.rawHeaders[i]!, req.rawHeaders[i + 1]!);
|
|
32
|
+
const method = req.method ?? "GET";
|
|
33
|
+
const response = await options.fetchImpl(ORIGIN + req.url, {
|
|
34
|
+
method, headers: forwardHeadersForUpstream(headers), redirect: "manual", signal: abort.signal,
|
|
35
|
+
// Node's and Bun's declarations disagree on BYOB overloads; both implement Web streams.
|
|
36
|
+
body: method === "GET" || method === "HEAD" ? undefined : Readable.toWeb(req) as unknown as ReadableStream<Uint8Array>,
|
|
37
|
+
// @ts-expect-error Node-compatible streaming fetch requires half duplex.
|
|
38
|
+
duplex: "half",
|
|
39
|
+
});
|
|
40
|
+
if (res.destroyed) { await response.body?.cancel(); return; }
|
|
41
|
+
const output: Record<string, string | string[]> = {};
|
|
42
|
+
response.headers.forEach((value, key) => { if (!STRIP.has(key) && key !== "set-cookie") output[key] = value; });
|
|
43
|
+
const cookies = response.headers.getSetCookie(); if (cookies.length) output["set-cookie"] = cookies;
|
|
44
|
+
res.writeHead(response.status, output);
|
|
45
|
+
if (!response.body || method === "HEAD") { await response.body?.cancel(); res.end(); return; }
|
|
46
|
+
const body = Readable.fromWeb(response.body as unknown as import("node:stream/web").ReadableStream<Uint8Array>);
|
|
47
|
+
body.once("error", () => res.destroy()); res.once("close", () => body.destroy()); body.pipe(res);
|
|
48
|
+
})().catch(() => {
|
|
49
|
+
if (res.headersSent) res.destroy(); else { res.writeHead(502, { "Content-Length": "0" }); res.end(); }
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
server.on("upgrade", (req, client, head) => {
|
|
53
|
+
if (!valid(req.headers.host, req.url)) { client.end("HTTP/1.1 421 Misdirected Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); return; }
|
|
54
|
+
const peer = options.websocketPeer;
|
|
55
|
+
const abort = new AbortController(); requests.add(abort);
|
|
56
|
+
client.once("close", () => { requests.delete(abort); abort.abort(); });
|
|
57
|
+
client.once("error", () => abort.abort());
|
|
58
|
+
void dialDesktopUpstream({ ...options.websocketTransport, ...(peer ? { target: { host: peer.host, port: peer.port }, ca: peer.ca } : {}), signal: abort.signal }).then(upstream => {
|
|
59
|
+
requests.delete(abort);
|
|
60
|
+
if (client.destroyed || abort.signal.aborted) { upstream.destroy(); return; }
|
|
61
|
+
sockets.add(upstream);
|
|
62
|
+
upstream.once("error", () => client.destroy());
|
|
63
|
+
upstream.once("close", () => { sockets.delete(upstream); client.destroy(); });
|
|
64
|
+
client.once("close", () => upstream.destroy());
|
|
65
|
+
const lines = [`${req.method ?? "GET"} ${req.url} HTTP/1.1`];
|
|
66
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
67
|
+
if (!/^proxy-(?:authorization|connection)$/i.test(req.rawHeaders[i]!)) lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
|
|
68
|
+
}
|
|
69
|
+
upstream.write(lines.join("\r\n") + "\r\n\r\n");
|
|
70
|
+
if (head.length) upstream.write(head);
|
|
71
|
+
client.pipe(upstream).pipe(client);
|
|
72
|
+
}).catch(() => {
|
|
73
|
+
requests.delete(abort);
|
|
74
|
+
if (!client.destroyed) client.end("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
await new Promise<void>((resolve, reject) => {
|
|
79
|
+
server.once("error", reject);
|
|
80
|
+
server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(); });
|
|
81
|
+
});
|
|
82
|
+
} catch (error) { server.close(); throw error; }
|
|
83
|
+
const address = server.address();
|
|
84
|
+
if (!address || typeof address === "string") throw new Error("desktop_compatibility_listener_unavailable");
|
|
85
|
+
let closing: Promise<void> | null = null;
|
|
86
|
+
return { port: address.port, close() {
|
|
87
|
+
if (closing) return closing;
|
|
88
|
+
for (const abort of requests) abort.abort();
|
|
89
|
+
for (const socket of sockets) socket.destroy();
|
|
90
|
+
closing = new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
|
|
91
|
+
server.closeAllConnections(); return closing;
|
|
92
|
+
} };
|
|
93
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
|
|
3
|
+
export interface NativeCompatibilityOwner { hostname: string; port: number; loopbackPort?: number; config: OcxConfig }
|
|
4
|
+
let owner: NativeCompatibilityOwner | null = null;
|
|
5
|
+
/** Bound sockets, not persisted desired ports, identify this process's native routing target. */
|
|
6
|
+
export function bindNativeCompatibilityOwner(value: NativeCompatibilityOwner): () => void {
|
|
7
|
+
owner = value;
|
|
8
|
+
return () => { if (owner === value) owner = null; };
|
|
9
|
+
}
|
|
10
|
+
export function nativeCompatibilityOwner(): NativeCompatibilityOwner | null { return owner; }
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { OcxConfig } from "../../types";
|
|
5
|
+
import { nativeCompatibilityOwner, type NativeCompatibilityOwner } from "./routing-binding";
|
|
6
|
+
|
|
7
|
+
const record = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
// These are routing inputs, not proof of a conversation's final provider. Their
|
|
9
|
+
// changes revoke the observed context even if the loopback URL stays the same.
|
|
10
|
+
const ROUTING_KEYS = ["providers", "defaultProvider", "defaultModelAliases", "customModels", "combos", "routingProfiles",
|
|
11
|
+
"subagentModelFallback", "subagentModelFallbackByModel", "injectionModel", "compactionRouting", "compactionRecovery",
|
|
12
|
+
"blockedModelRedirects", "shadowCallIntercept", "protocols", "memoryModels"] as const satisfies readonly (keyof OcxConfig)[];
|
|
13
|
+
function routingDigest(text: string, owner: NativeCompatibilityOwner): string {
|
|
14
|
+
const native = Bun.TOML.parse(text) as Record<string, unknown>;
|
|
15
|
+
// Desktop refreshes MCP endpoints after launch. Tool transport metadata and TOML
|
|
16
|
+
// formatting do not select the model transport. Keep all other (including unknown)
|
|
17
|
+
// native fields bound, rather than an allowlist that could miss a new routing key.
|
|
18
|
+
const { mcp_servers: _mcpServers, ...nativeContext } = native;
|
|
19
|
+
const projection = Object.fromEntries(ROUTING_KEYS.map(key => [key, owner.config[key]]));
|
|
20
|
+
const serialized = JSON.stringify([nativeContext, owner.hostname, owner.port, owner.loopbackPort, projection], (_key, value) =>
|
|
21
|
+
record(value) ? Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]])) : value);
|
|
22
|
+
if (Buffer.byteLength(serialized) > 1048576) throw new Error("Routing snapshot too large");
|
|
23
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
/** Validates the effective root/profile routing only; it does not prove a thread's selected provider. */
|
|
26
|
+
export function matchesNativeCompatibilityRouting(text: string, owner: NativeCompatibilityOwner | null): boolean {
|
|
27
|
+
if (!owner || owner.config.codexDesktopAuthless === true || owner.config.runtimeRole === "client") return false;
|
|
28
|
+
try {
|
|
29
|
+
const root: unknown = Bun.TOML.parse(text);
|
|
30
|
+
if (!record(root)) return false;
|
|
31
|
+
let effective = root;
|
|
32
|
+
if (root.profile !== undefined) {
|
|
33
|
+
if (typeof root.profile !== "string" || !record(root.profiles)) return false;
|
|
34
|
+
const profile = root.profiles[root.profile];
|
|
35
|
+
if (!record(profile)) return false;
|
|
36
|
+
effective = { ...root, ...profile };
|
|
37
|
+
}
|
|
38
|
+
if ((effective.model_provider ?? "openai") !== "openai" || effective.forced_login_method === "api") return false;
|
|
39
|
+
if (typeof effective.openai_base_url !== "string") return false;
|
|
40
|
+
const url = new URL(effective.openai_base_url);
|
|
41
|
+
if (url.protocol !== "http:" || !["127.0.0.1", "[::1]"].includes(url.hostname) || url.username || url.password
|
|
42
|
+
|| url.search || url.hash || !["/v1", "/v1/"].includes(url.pathname) || !url.port) return false;
|
|
43
|
+
const port = Number(url.port), matchesPort = (value: number | undefined) => Number.isInteger(value) && Number(value) > 0 && value === port;
|
|
44
|
+
// The companion always binds IPv4. localhost can resolve to another address family.
|
|
45
|
+
if (matchesPort(owner.loopbackPort) && url.hostname === "127.0.0.1") return true;
|
|
46
|
+
if (!matchesPort(owner.port)) return false;
|
|
47
|
+
return url.hostname === "127.0.0.1" ? ["127.0.0.1", "0.0.0.0"].includes(owner.hostname)
|
|
48
|
+
: ["::1", "[::1]", "::", "[::]"].includes(owner.hostname);
|
|
49
|
+
} catch { return false; }
|
|
50
|
+
}
|
|
51
|
+
export function createNativeRoutingVerifier(codexHome: string, readOwner = nativeCompatibilityOwner) {
|
|
52
|
+
const path = join(codexHome, "config.toml");
|
|
53
|
+
let baseline: string | undefined, invalidated = false;
|
|
54
|
+
return () => {
|
|
55
|
+
if (invalidated) return false;
|
|
56
|
+
try {
|
|
57
|
+
// Process-level overrides could put the app on another transport despite its TOML.
|
|
58
|
+
if (["CODEX_API_BASE_URL", "CODEX_APP_SERVER_WS_URL", "CODEX_ELECTRON_USER_DATA_PATH", "ELECTRON_RUN_AS_NODE"].some(key => process.env[key]?.trim())) { invalidated = true; return false; }
|
|
59
|
+
const stat = lstatSync(path);
|
|
60
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576) { invalidated = true; return false; }
|
|
61
|
+
const text = readFileSync(path, "utf8"), owner = readOwner();
|
|
62
|
+
if (!owner || !matchesNativeCompatibilityRouting(text, owner)) { invalidated = true; return false; }
|
|
63
|
+
const current = routingDigest(text, owner);
|
|
64
|
+
if (baseline !== undefined && current !== baseline) { invalidated = true; return false; }
|
|
65
|
+
baseline ??= current;
|
|
66
|
+
return true;
|
|
67
|
+
} catch { invalidated = true; return false; }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
interface LaunchContext { pacUrl: string; generation: string; current(): boolean }
|
|
4
|
+
/** Process-local ownership; the management sibling guard owns the cross-instance boundary. */
|
|
5
|
+
let owner: { token: symbol; kind: "runtime" | "certificate"; launch?: LaunchContext } | null = null;
|
|
6
|
+
export function desktopCompatibilityRuntimeActive(): boolean { return owner?.kind === "runtime"; }
|
|
7
|
+
function acquire(kind: "runtime" | "certificate"): () => void {
|
|
8
|
+
if (owner !== null) throw new Error("desktop_compatibility_busy");
|
|
9
|
+
const token = Symbol(); owner = { token, kind };
|
|
10
|
+
return () => { if (owner?.token === token) owner = null; };
|
|
11
|
+
}
|
|
12
|
+
export const acquireDesktopCompatibilityRuntime = () => acquire("runtime");
|
|
13
|
+
export const acquireDesktopCertificateMutation = () => acquire("certificate");
|
|
14
|
+
|
|
15
|
+
/** Published only by a successfully started runtime, never reconstructed from disk or app arguments. */
|
|
16
|
+
export function bindDesktopCompatibilityLaunch(pacUrl: string, current: () => boolean): () => void {
|
|
17
|
+
if (owner?.kind !== "runtime" || owner.launch) throw new Error("desktop_compatibility_launch_owner_unverified");
|
|
18
|
+
const lease = owner, launch = { pacUrl, generation: randomUUID(), current };
|
|
19
|
+
lease.launch = launch;
|
|
20
|
+
return () => { if (lease.launch === launch) delete lease.launch; };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** No key loading, socket discovery, or optional-runtime construction on the ordinary restart path. */
|
|
24
|
+
export function readDesktopCompatibilityLaunch(): { pacUrl: string; generation: string } | null {
|
|
25
|
+
const launch = owner?.kind === "runtime" ? owner.launch : undefined;
|
|
26
|
+
try { return launch?.current() ? { pacUrl: launch.pacUrl, generation: launch.generation } : null; }
|
|
27
|
+
catch { return null; }
|
|
28
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getConfigDir } from "../../config/paths";
|
|
4
|
+
import { issueServerLeaf } from "../../claude/intercept/local-ca";
|
|
5
|
+
import { startConnectProxy, type ConnectProxyHandle } from "../../claude/intercept/connect-proxy";
|
|
6
|
+
import { didRunOptionalShutdownHooks, registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks";
|
|
7
|
+
import { isTestHomeGuardArmed } from "../../lib/test-home-guard";
|
|
8
|
+
import { desktopOutboundFetch, desktopProxyFor } from "../../lib/desktop-proxy-route";
|
|
9
|
+
import { inspectDesktopCompatibilityAuthority, loadDesktopCompatibilityAuthority, type StoredDesktopAuthority } from "./certificate-store";
|
|
10
|
+
import { inspectWindowsCertificateTrust } from "./windows-certificate-trust";
|
|
11
|
+
import { createNativeIdentityReader } from "./native-identity";
|
|
12
|
+
import { UsageRelayController, type UsageIdentity } from "./usage-controller";
|
|
13
|
+
import { createUsageControlledFetch } from "./usage-controlled-fetch";
|
|
14
|
+
import { startDesktopRelay } from "./relay-listener";
|
|
15
|
+
import { launchWindowsCodexCompatibility } from "./windows-package-launch";
|
|
16
|
+
import { acquireDesktopCompatibilityRuntime, bindDesktopCompatibilityLaunch } from "./runtime-ownership";
|
|
17
|
+
import { createDesktopConnectionStore, type DesktopConnectionStore } from "./connection-store";
|
|
18
|
+
import { createNativeRoutingVerifier } from "./routing-preflight";
|
|
19
|
+
import { createInstalledBuildProbe } from "./installed-build";
|
|
20
|
+
|
|
21
|
+
export { DESKTOP_COMPATIBILITY_ASSESSED_VERSION, isAssessedDesktopInstalled } from "./installed-build";
|
|
22
|
+
export interface DesktopRuntimeIo {
|
|
23
|
+
platform?: string;
|
|
24
|
+
loadAuthority?: () => Promise<StoredDesktopAuthority>;
|
|
25
|
+
trust?: (authority: StoredDesktopAuthority) => Promise<"trusted" | "not-trusted" | "unknown">;
|
|
26
|
+
identity?: ReturnType<typeof createNativeIdentityReader>;
|
|
27
|
+
buildSupported?: () => boolean | Promise<boolean>;
|
|
28
|
+
upstreamFetch?: typeof fetch;
|
|
29
|
+
connectionStore?: DesktopConnectionStore;
|
|
30
|
+
routingSupported?: () => boolean;
|
|
31
|
+
/** Synthetic-test execution is admitted only with explicit fixture identity and CA seams. */
|
|
32
|
+
testOnly?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Optional runtime. Import/construction/status do not start listeners or touch credentials. */
|
|
36
|
+
export function createDesktopCompatibilityRuntime(io: DesktopRuntimeIo = {}) {
|
|
37
|
+
const platform = io.platform ?? process.platform;
|
|
38
|
+
let buildProbe: ReturnType<typeof createInstalledBuildProbe> | undefined;
|
|
39
|
+
const buildSupported = io.buildSupported ?? (() => buildProbe?.check() ?? Promise.resolve(false));
|
|
40
|
+
let routingSupported = io.routingSupported ?? (() => false);
|
|
41
|
+
let contextFailure: "build_unverified" | "native_routing_unverified" | null = null;
|
|
42
|
+
const contextValid = async () => {
|
|
43
|
+
try {
|
|
44
|
+
if (!routingSupported()) { contextFailure = "native_routing_unverified"; return false; }
|
|
45
|
+
const supported = await buildSupported();
|
|
46
|
+
contextFailure = !routingSupported() ? "native_routing_unverified" : !supported ? "build_unverified" : null;
|
|
47
|
+
}
|
|
48
|
+
catch { contextFailure = "build_unverified"; }
|
|
49
|
+
return contextFailure === null;
|
|
50
|
+
};
|
|
51
|
+
let phase: "off" | "starting" | "running" | "stopping" | "cleanup-required" = "off";
|
|
52
|
+
let owned: { controller: UsageRelayController; close(): Promise<void>; pacUrl: string; fingerprint: string; deadline: number } | null = null;
|
|
53
|
+
let closing: Promise<void> | null = null;
|
|
54
|
+
let pendingCleanup: (() => Promise<void>) | null = null;
|
|
55
|
+
let releaseOwner: (() => void) | null = null;
|
|
56
|
+
const status = () => ({ phase, supported: platform === "win32", running: owned !== null, contextFailure,
|
|
57
|
+
...(owned ? { fingerprint: owned.fingerprint, safetyDeadline: owned.deadline, usage: owned.controller.snapshot() } : {}) });
|
|
58
|
+
async function stop(): Promise<ReturnType<typeof status>> {
|
|
59
|
+
if (phase === "starting") throw new Error("desktop_compatibility_busy");
|
|
60
|
+
if (closing) { await closing; return status(); }
|
|
61
|
+
if (!pendingCleanup) return status();
|
|
62
|
+
phase = "stopping";
|
|
63
|
+
closing = pendingCleanup().then(() => { owned = null; pendingCleanup = null; phase = "off"; releaseOwner?.(); releaseOwner = null; })
|
|
64
|
+
.catch(error => { phase = "cleanup-required"; throw error; }).finally(() => { closing = null; });
|
|
65
|
+
await closing; return status();
|
|
66
|
+
}
|
|
67
|
+
async function start() {
|
|
68
|
+
if (phase !== "off") throw new Error("desktop_compatibility_busy");
|
|
69
|
+
if (platform !== "win32") throw new Error("desktop_compatibility_unsupported");
|
|
70
|
+
if (isTestHomeGuardArmed() && !(io.testOnly && io.identity && io.loadAuthority && io.trust && io.buildSupported && io.connectionStore && io.routingSupported)) throw new Error("desktop_compatibility_test_environment");
|
|
71
|
+
if (didRunOptionalShutdownHooks()) throw new Error("desktop_compatibility_stopping");
|
|
72
|
+
try { desktopProxyFor(new URL("https://chatgpt.com")); } catch { throw new Error("desktop_compatibility_egress_proxy_invalid"); }
|
|
73
|
+
releaseOwner = acquireDesktopCompatibilityRuntime(); phase = "starting";
|
|
74
|
+
if (!io.buildSupported) buildProbe = createInstalledBuildProbe();
|
|
75
|
+
let relay: Awaited<ReturnType<typeof startDesktopRelay>> | undefined, proxy: ConnectProxyHandle | undefined;
|
|
76
|
+
let pac: ReturnType<typeof Bun.serve> | undefined, timer: ReturnType<typeof setInterval> | undefined;
|
|
77
|
+
let detach: (() => void) | undefined, unbindLaunch: (() => void) | undefined, controller: UsageRelayController | undefined;
|
|
78
|
+
const cleanup = async () => {
|
|
79
|
+
unbindLaunch?.();
|
|
80
|
+
if (timer) clearInterval(timer); detach?.();
|
|
81
|
+
// Abort transport before awaiting stream refresh so a stuck reader cannot retain sockets.
|
|
82
|
+
const results = await Promise.allSettled([pac?.stop(true), proxy?.close(), relay?.close(), controller?.observeOnly(), buildProbe?.close()]);
|
|
83
|
+
if (results.some(result => result.status === "rejected")) throw new Error("desktop_compatibility_cleanup_incomplete");
|
|
84
|
+
};
|
|
85
|
+
pendingCleanup = cleanup;
|
|
86
|
+
try {
|
|
87
|
+
if (!io.routingSupported) routingSupported = createNativeRoutingVerifier((await import("../paths")).getCodexHome());
|
|
88
|
+
if (!await contextValid()) throw new Error(`desktop_compatibility_${contextFailure}`);
|
|
89
|
+
const directory = join(getConfigDir(), "codex-desktop-compatibility");
|
|
90
|
+
if (!io.loadAuthority) {
|
|
91
|
+
const saved = inspectDesktopCompatibilityAuthority(directory);
|
|
92
|
+
if (saved.status === "missing") throw new Error("desktop_compatibility_certificate_not_prepared");
|
|
93
|
+
if (saved.status === "invalid") throw new Error("desktop_compatibility_certificate_invalid");
|
|
94
|
+
if (saved.status === "expired") throw new Error("desktop_compatibility_certificate_expired");
|
|
95
|
+
}
|
|
96
|
+
const authority = await (io.loadAuthority?.() ?? loadDesktopCompatibilityAuthority({ directory }));
|
|
97
|
+
const trust = await (io.trust?.(authority) ?? inspectWindowsCertificateTrust(authority.authority.certPem, authority.fingerprint));
|
|
98
|
+
if (trust !== "trusted") throw new Error("desktop_compatibility_trust_required");
|
|
99
|
+
const connections = io.connectionStore ?? createDesktopConnectionStore(directory);
|
|
100
|
+
const previousConnection = connections.read();
|
|
101
|
+
const deadline = authority.expiresAt - 300_000;
|
|
102
|
+
if (deadline <= Date.now()) throw new Error("desktop_compatibility_certificate_expiring");
|
|
103
|
+
const identity = io.identity ?? createNativeIdentityReader(join((await import("../paths")).getCodexHome(), "auth.json"), desktopOutboundFetch as typeof fetch);
|
|
104
|
+
const account: UsageIdentity | null = await identity.verifyFreshIdentity();
|
|
105
|
+
if (!account) throw new Error("desktop_compatibility_native_identity_unverified");
|
|
106
|
+
if (didRunOptionalShutdownHooks()) throw new Error("desktop_compatibility_stopping");
|
|
107
|
+
controller = new UsageRelayController(account, identity.readCurrentIdentity, identity.verifyFreshIdentity, Date.now, deadline, 180000, contextValid);
|
|
108
|
+
relay = await startDesktopRelay({ leaf: issueServerLeaf(authority.authority, authority.commonName, ["chatgpt.com"]),
|
|
109
|
+
fetchImpl: createUsageControlledFetch(controller, io.upstreamFetch ?? desktopOutboundFetch as typeof fetch) });
|
|
110
|
+
try { proxy = await startConnectProxy(previousConnection?.connectPort ?? 0, { interceptPort: relay.port, interceptHosts: ["chatgpt.com"], allowedTargets: ["chatgpt.com:443"] }); }
|
|
111
|
+
catch { throw new Error("desktop_compatibility_connection_unavailable"); }
|
|
112
|
+
const runId = previousConnection?.id ?? randomUUID(), proxyPort = proxy.port;
|
|
113
|
+
try { pac = Bun.serve({ hostname: "127.0.0.1", port: previousConnection?.pacPort ?? 0, fetch(req) {
|
|
114
|
+
const url = new URL(req.url);
|
|
115
|
+
if (req.method !== "GET" || req.headers.has("origin") || url.origin !== `http://127.0.0.1:${pac!.port}` || url.pathname !== `/${runId}/proxy.pac`) return new Response(null, { status: 404 });
|
|
116
|
+
return new Response(`function FindProxyForURL(url, host) { return Date.now() < ${deadline} && host.toLowerCase() === "chatgpt.com" && url.indexOf("https:") === 0 ? "PROXY 127.0.0.1:${proxyPort}; DIRECT" : "DIRECT"; }`,
|
|
117
|
+
{ headers: { "content-type": "application/x-ns-proxy-autoconfig", "cache-control": "no-store" } });
|
|
118
|
+
} }); } catch { throw new Error("desktop_compatibility_connection_unavailable"); }
|
|
119
|
+
const connection = await connections.publish({ version: 1, id: runId, connectPort: proxyPort, pacPort: pac.port! });
|
|
120
|
+
if (connection.id !== runId || connection.connectPort !== proxyPort || connection.pacPort !== pac.port) throw new Error("desktop_compatibility_connection_changed");
|
|
121
|
+
if (didRunOptionalShutdownHooks()) throw new Error("desktop_compatibility_stopping");
|
|
122
|
+
const active = controller; let ticking = false, deadlineHandled = false, lastContextCheck = Date.now();
|
|
123
|
+
timer = setInterval(() => {
|
|
124
|
+
if (ticking) return; ticking = true;
|
|
125
|
+
const expired = Date.now() >= deadline;
|
|
126
|
+
const work = (async () => {
|
|
127
|
+
const contextCadence = active.snapshot().mode === "apply" ? 10000 : 60000;
|
|
128
|
+
if (!expired && Date.now() - lastContextCheck >= contextCadence) {
|
|
129
|
+
lastContextCheck = Date.now();
|
|
130
|
+
if (!await contextValid() && active.snapshot().mode === "apply") { await active.observeOnly(); return; }
|
|
131
|
+
}
|
|
132
|
+
await (expired && !deadlineHandled ? (deadlineHandled = true, active.observeOnly()) : active.expireIfNeeded());
|
|
133
|
+
})();
|
|
134
|
+
void work.catch(() => {}).finally(() => { ticking = false; });
|
|
135
|
+
}, 1000); timer.unref();
|
|
136
|
+
owned = { controller, close: cleanup, fingerprint: authority.fingerprint, deadline, pacUrl: `http://127.0.0.1:${pac.port}/${runId}/proxy.pac` };
|
|
137
|
+
phase = "running";
|
|
138
|
+
const launchedBy = owned;
|
|
139
|
+
unbindLaunch = bindDesktopCompatibilityLaunch(owned.pacUrl,
|
|
140
|
+
() => owned === launchedBy && phase === "running" && Date.now() < deadline);
|
|
141
|
+
detach = registerOptionalShutdownHook("codex-desktop-compatibility", () => { void stop().catch(() => {}); });
|
|
142
|
+
return status();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
try { await cleanup(); pendingCleanup = null; phase = "off"; releaseOwner?.(); releaseOwner = null; }
|
|
145
|
+
catch { phase = "cleanup-required"; throw new Error("desktop_compatibility_cleanup_incomplete"); }
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function apply(accountWideConsent: boolean) {
|
|
150
|
+
const current = owned;
|
|
151
|
+
if (!current || phase !== "running") throw new Error("desktop_compatibility_not_running");
|
|
152
|
+
const generation = current.controller.snapshot().generation, valid = await contextValid();
|
|
153
|
+
if (owned !== current || phase !== "running") throw new Error("desktop_compatibility_not_running");
|
|
154
|
+
if (generation !== current.controller.snapshot().generation) return { accepted: false, reason: "superseded", ...current.controller.snapshot() };
|
|
155
|
+
if (!valid) { await current.controller.observeOnly(); throw new Error(`desktop_compatibility_${contextFailure}`); }
|
|
156
|
+
const result = await current.controller.activate({ scope: "account-ui-compatibility", accountWideConsent });
|
|
157
|
+
if (owned !== current || phase !== "running") { await current.controller.observeOnly(); return { ...result, accepted: false, reason: "runtime-stopped" }; }
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
return { status, start, stop, apply,
|
|
161
|
+
async observe() { if (owned) await owned.controller.observeOnly(); return status(); },
|
|
162
|
+
async launch() {
|
|
163
|
+
if (!owned || phase !== "running") throw new Error("desktop_compatibility_not_running");
|
|
164
|
+
const current = owned;
|
|
165
|
+
if (!await contextValid()) throw new Error(`desktop_compatibility_${contextFailure}`);
|
|
166
|
+
if (owned !== current || phase !== "running") throw new Error("desktop_compatibility_not_running");
|
|
167
|
+
return launchWindowsCodexCompatibility(current.pacUrl);
|
|
168
|
+
},
|
|
169
|
+
/** Internal native launch context; management status never returns the control endpoint. */
|
|
170
|
+
getPacUrl: () => owned?.pacUrl ?? null,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export type DesktopCompatibilityRuntime = ReturnType<typeof createDesktopCompatibilityRuntime>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createDesktopCompatibilityRuntime, type DesktopCompatibilityRuntime } from "./runtime";
|
|
2
|
+
|
|
3
|
+
/** One runtime for management and automatic startup; shutdown never constructs a new instance. */
|
|
4
|
+
export function createDesktopCompatibilityService(factory: () => DesktopCompatibilityRuntime = createDesktopCompatibilityRuntime) {
|
|
5
|
+
let runtime: DesktopCompatibilityRuntime | undefined;
|
|
6
|
+
let starting: Promise<ReturnType<DesktopCompatibilityRuntime["status"]>> | null = null;
|
|
7
|
+
let shuttingDown = false;
|
|
8
|
+
return {
|
|
9
|
+
get(): DesktopCompatibilityRuntime {
|
|
10
|
+
if (!runtime) {
|
|
11
|
+
const raw = factory();
|
|
12
|
+
runtime = { ...raw, start() {
|
|
13
|
+
if (shuttingDown) return Promise.reject(new Error("desktop_compatibility_stopping"));
|
|
14
|
+
if (starting) return Promise.reject(new Error("desktop_compatibility_busy"));
|
|
15
|
+
starting = raw.start().finally(() => { starting = null; });
|
|
16
|
+
return starting;
|
|
17
|
+
} };
|
|
18
|
+
}
|
|
19
|
+
return runtime;
|
|
20
|
+
},
|
|
21
|
+
async shutdown(): Promise<void> {
|
|
22
|
+
shuttingDown = true;
|
|
23
|
+
try { await starting; } catch { /* failed start owns its cleanup */ }
|
|
24
|
+
if (runtime) await runtime.stop();
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const service = createDesktopCompatibilityService();
|
|
29
|
+
export const getDesktopCompatibilityRuntime = () => service.get();
|
|
30
|
+
export const shutdownDesktopCompatibility = () => service.shutdown();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readConfigFileSnapshot } from "../../config/diagnostics";
|
|
3
|
+
import { mutatePersistedConfig } from "../../config/persisted-mutation";
|
|
4
|
+
import { desktopCompatibilityConfigError } from "../../config/schema/desktop-compatibility";
|
|
5
|
+
import { adoptPersistedDesktopCompatibility } from "../../config/live-reconcile";
|
|
6
|
+
import type { OcxConfig } from "../../types";
|
|
7
|
+
|
|
8
|
+
export interface DesktopStartupSettings { startOnProxyStart: boolean; revision: string }
|
|
9
|
+
interface SettingsIo { read?: typeof readConfigFileSnapshot; mutate?: typeof mutatePersistedConfig; liveConfig?: OcxConfig }
|
|
10
|
+
const failure = (code: string) => new Error(`desktop_compatibility_settings_${code}`);
|
|
11
|
+
/** A next-process preference: never changes live runtime, trust, app or login state. */
|
|
12
|
+
export function createDesktopStartupSettings(io: SettingsIo = {}) {
|
|
13
|
+
const read = io.read ?? readConfigFileSnapshot, mutate = io.mutate ?? mutatePersistedConfig;
|
|
14
|
+
function inspect() {
|
|
15
|
+
const snapshot = read();
|
|
16
|
+
if (snapshot.diagnostics.source !== "file" || snapshot.raw === undefined) throw failure("unavailable");
|
|
17
|
+
let raw: Record<string, unknown>;
|
|
18
|
+
try { raw = JSON.parse(snapshot.raw.replace(/^\uFEFF/, "")); } catch { throw failure("unavailable"); }
|
|
19
|
+
if (desktopCompatibilityConfigError(raw)) throw failure("invalid");
|
|
20
|
+
const setting = raw.desktopCompatibility as { startOnProxyStart: boolean } | undefined;
|
|
21
|
+
return { setting, status: { startOnProxyStart: setting?.startOnProxyStart === true,
|
|
22
|
+
revision: createHash("sha256").update(JSON.stringify(setting ?? null)).digest("hex") } };
|
|
23
|
+
}
|
|
24
|
+
const status = (): DesktopStartupSettings => inspect().status;
|
|
25
|
+
function set(startOnProxyStart: boolean, expectedRevision: string): DesktopStartupSettings {
|
|
26
|
+
if (typeof startOnProxyStart !== "boolean" || !/^[a-f0-9]{64}$/.test(expectedRevision)) throw failure("invalid_request");
|
|
27
|
+
let outcome;
|
|
28
|
+
try { outcome = mutate(config => {
|
|
29
|
+
// Runs under the existing config lock, including each rebase retry. Unrelated fields
|
|
30
|
+
// may change, but a changed preference must be shown again before this write.
|
|
31
|
+
const current = status();
|
|
32
|
+
if (current.revision !== expectedRevision) throw failure("changed");
|
|
33
|
+
if (current.startOnProxyStart === startOnProxyStart) return { changed: false, value: startOnProxyStart };
|
|
34
|
+
config.desktopCompatibility = { startOnProxyStart };
|
|
35
|
+
return { changed: true, value: startOnProxyStart };
|
|
36
|
+
}); } catch (error) {
|
|
37
|
+
// Publication can succeed before bookkeeping fails. Reconcile a verified disk
|
|
38
|
+
// field without claiming success or replaying the uncertain write.
|
|
39
|
+
if (io.liveConfig) { try { adoptPersistedDesktopCompatibility(io.liveConfig, inspect().setting); } catch { /* unresolved state remains an error */ } }
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
if (outcome.status === "unavailable") throw failure("unavailable");
|
|
43
|
+
const actual = inspect();
|
|
44
|
+
if (io.liveConfig) adoptPersistedDesktopCompatibility(io.liveConfig, actual.setting);
|
|
45
|
+
if (actual.status.startOnProxyStart !== startOnProxyStart) throw failure("changed");
|
|
46
|
+
return actual.status;
|
|
47
|
+
}
|
|
48
|
+
return { status, set };
|
|
49
|
+
}
|
|
50
|
+
export type DesktopStartupSettingsService = ReturnType<typeof createDesktopStartupSettings>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Account-bound, time-limited compatibility activation; no persistent settings or timers. */
|
|
2
|
+
export type Observation = 'exhausted' | 'available' | 'protected';
|
|
3
|
+
export type ApplyScope = 'selected-external-model' | 'account-ui-compatibility';
|
|
4
|
+
type RefreshResult = {requested:number;closed:number;failed:number};
|
|
5
|
+
export class UsageActivation {
|
|
6
|
+
private mode: 'observe' | 'apply' = 'observe';
|
|
7
|
+
private phase = 'observing';
|
|
8
|
+
private generation = 0;
|
|
9
|
+
private latest: { kind:Observation; at:number } | null = null;
|
|
10
|
+
private startedAt: number | null = null;
|
|
11
|
+
private outputs = 0;
|
|
12
|
+
constructor(private binding:string, private refresh:(binding:string)=>Promise<RefreshResult>,
|
|
13
|
+
private verifyIdentity:()=>Promise<string|null>,
|
|
14
|
+
private clock:()=>number=Date.now, private timeoutMs=180000) {
|
|
15
|
+
if(!binding || !Number.isSafeInteger(timeoutMs) || timeoutMs<1 || timeoutMs>180000) throw new Error('Invalid activation limits');
|
|
16
|
+
}
|
|
17
|
+
snapshot() { return {mode:this.mode,phase:this.phase,generation:this.generation,outputs:this.outputs,
|
|
18
|
+
appCacheConfirmed:false,requestedUiScope:'account-wide',providerIsolationAvailable:false}; }
|
|
19
|
+
invalidateIdentity() {
|
|
20
|
+
this.mode='observe';this.phase='identity-invalidated';this.startedAt=null;this.latest=null;this.outputs=0;++this.generation;
|
|
21
|
+
}
|
|
22
|
+
observe(binding:string,kind:Observation) {
|
|
23
|
+
if(binding!==this.binding)return false;
|
|
24
|
+
this.latest={kind,at:this.clock()};
|
|
25
|
+
// A recovered or protected account must not silently re-enter a previous trial
|
|
26
|
+
// if a later snapshot becomes exhausted again. A new trial requires consent.
|
|
27
|
+
if(this.mode==='apply'&&kind!=='exhausted'){
|
|
28
|
+
this.mode='observe';this.phase=kind==='available'?'usage-recovered':'protected-observation';
|
|
29
|
+
this.startedAt=null;this.outputs=0;++this.generation;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
private async reset(phase:string) {
|
|
34
|
+
this.mode='observe';this.phase=phase;this.startedAt=null;this.latest=null;this.outputs=0;++this.generation;
|
|
35
|
+
return this.refresh(this.binding);
|
|
36
|
+
}
|
|
37
|
+
async activate(options:{scope:ApplyScope;accountWideConsent:boolean}) {
|
|
38
|
+
if(options.scope!=='account-ui-compatibility')return{accepted:false,reason:'selected-model-scope-unavailable',...this.snapshot()};
|
|
39
|
+
if(!options.accountWideConsent)return{accepted:false,reason:'account-wide-scope-not-accepted',...this.snapshot()};
|
|
40
|
+
const beforeVerification=this.generation,binding=this.binding;
|
|
41
|
+
let verified:string|null;
|
|
42
|
+
try{verified=await this.verifyIdentity();}catch{verified=null;}
|
|
43
|
+
if(verified!==binding||this.binding!==binding||this.generation!==beforeVerification)return{accepted:false,reason:'identity-not-verified',...this.snapshot()};
|
|
44
|
+
if(this.mode==='apply')return{accepted:false,reason:'activation-already-pending',...this.snapshot()};
|
|
45
|
+
const age=this.latest?this.clock()-this.latest.at:Infinity;
|
|
46
|
+
if(!this.latest||this.latest.kind!=='exhausted'||age<0||age>300000)return{accepted:false,reason:'no-fresh-eligible-exhaustion',...this.snapshot()};
|
|
47
|
+
// An exhausted observation authorizes one trial, not every retry within its TTL.
|
|
48
|
+
this.latest=null;
|
|
49
|
+
this.mode='apply';this.phase='awaiting-fresh-usage';this.startedAt=this.clock();this.outputs=0;
|
|
50
|
+
const generation=++this.generation;
|
|
51
|
+
let refreshed:RefreshResult;
|
|
52
|
+
try { refreshed=await this.refresh(this.binding); }
|
|
53
|
+
catch {
|
|
54
|
+
if(this.generation===generation){this.mode='observe';this.phase='refresh-failed';this.startedAt=null;this.latest=null;this.outputs=0;++this.generation;}
|
|
55
|
+
return{accepted:false,reason:'refresh-failed',...this.snapshot()};
|
|
56
|
+
}
|
|
57
|
+
// Account/mode changes while a close callback awaited invalidate this activation.
|
|
58
|
+
if(this.generation!==generation)return{accepted:false,reason:'superseded',...this.snapshot()};
|
|
59
|
+
// The registry settles every close and reports failures; it does not reject the batch.
|
|
60
|
+
// Partial closure cannot leave rewriting enabled while an old app cache may survive.
|
|
61
|
+
if(!Number.isSafeInteger(refreshed.requested)||!Number.isSafeInteger(refreshed.closed)
|
|
62
|
+
||!Number.isSafeInteger(refreshed.failed)||refreshed.requested<0||refreshed.closed<0
|
|
63
|
+
||refreshed.failed!==0||refreshed.closed!==refreshed.requested){
|
|
64
|
+
this.mode='observe';this.phase='refresh-failed';this.startedAt=null;this.latest=null;this.outputs=0;++this.generation;
|
|
65
|
+
return{accepted:false,reason:'refresh-failed',refresh:refreshed,...this.snapshot()};
|
|
66
|
+
}
|
|
67
|
+
return{accepted:true,reason:'awaiting-new-response',refresh:refreshed,...this.snapshot()};
|
|
68
|
+
}
|
|
69
|
+
recordOutput(binding:string,generation:number) {
|
|
70
|
+
if(binding!==this.binding||generation!==this.generation||this.mode!=='apply')return false;
|
|
71
|
+
if(this.startedAt===null||this.clock()-this.startedAt<0||this.clock()-this.startedAt>=this.timeoutMs)return false;
|
|
72
|
+
++this.outputs;this.phase='response-produced-app-confirmation-needed';return true;
|
|
73
|
+
}
|
|
74
|
+
async expireIfNeeded() {
|
|
75
|
+
if(this.startedAt===null||this.clock()-this.startedAt<this.timeoutMs&&this.clock()-this.startedAt>=0)return false;
|
|
76
|
+
await this.reset('expired-awaiting-original-response');return true;
|
|
77
|
+
}
|
|
78
|
+
async observeOnly() { return this.reset('observing-awaiting-original-response'); }
|
|
79
|
+
async changeBinding(binding:string) {
|
|
80
|
+
if(!binding)throw new Error('Verified account binding is required');
|
|
81
|
+
const previous=this.binding;
|
|
82
|
+
this.mode='observe';this.phase='account-changed';this.startedAt=null;this.latest=null;this.outputs=0;++this.generation;
|
|
83
|
+
this.binding=binding;
|
|
84
|
+
return this.refresh(previous);
|
|
85
|
+
}
|
|
86
|
+
}
|