codex-multi-auth 2.17.0 → 2.17.3
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/.codex-plugin/plugin.json +9 -9
- package/LICENSE +21 -21
- package/README.md +142 -484
- package/assets/codex-multi-auth-icon.svg +39 -39
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +31 -31
- package/config/codex-legacy.json +1221 -1221
- package/config/codex-modern.json +423 -423
- package/config/minimal-codex.json +15 -15
- package/dist/index.d.ts +0 -1
- package/dist/lib/account-policy.d.ts +18 -2
- package/dist/lib/account-policy.js +97 -40
- package/dist/lib/accounts/rate-limits.d.ts +0 -1
- package/dist/lib/accounts.d.ts +1 -8
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +7 -19
- package/dist/lib/api-route-store.d.ts +0 -1
- package/dist/lib/audit.d.ts +0 -1
- package/dist/lib/audit.js +43 -12
- package/dist/lib/auth/account-access.d.ts +0 -1
- package/dist/lib/auth/account-access.js +5 -1
- package/dist/lib/auth/auth.d.ts +4 -3
- package/dist/lib/auth/auth.js +220 -28
- package/dist/lib/auth/browser.d.ts +0 -1
- package/dist/lib/auth/callback-guidance.d.ts +0 -1
- package/dist/lib/auth/device-auth.d.ts +0 -1
- package/dist/lib/auth/index.d.ts +0 -1
- package/dist/lib/auth/org-override.d.ts +0 -1
- package/dist/lib/auth/server.d.ts +1 -2
- package/dist/lib/auth/server.js +100 -53
- package/dist/lib/auth/token-utils.d.ts +0 -1
- package/dist/lib/auth-rate-limit.d.ts +0 -1
- package/dist/lib/budget-guard.d.ts +16 -2
- package/dist/lib/budget-guard.js +123 -43
- package/dist/lib/capability-policy.d.ts +0 -1
- package/dist/lib/circuit-breaker.d.ts +20 -1
- package/dist/lib/circuit-breaker.js +24 -5
- package/dist/lib/cli.d.ts +0 -1
- package/dist/lib/codex-cli/index.d.ts +0 -1
- package/dist/lib/codex-cli/observability.d.ts +0 -1
- package/dist/lib/codex-cli/state.d.ts +0 -1
- package/dist/lib/codex-cli/sync.d.ts +0 -1
- package/dist/lib/codex-cli/writer.d.ts +0 -1
- package/dist/lib/codex-manager/account-credentials.d.ts +0 -1
- package/dist/lib/codex-manager/account-manager-commands.d.ts +0 -1
- package/dist/lib/codex-manager/account-pool-write.d.ts +0 -1
- package/dist/lib/codex-manager/active-account-sync.d.ts +6 -0
- package/dist/lib/codex-manager/active-account-sync.js +336 -0
- package/dist/lib/codex-manager/api-login-menu.d.ts +0 -1
- package/dist/lib/codex-manager/backend-category-entry.d.ts +0 -1
- package/dist/lib/codex-manager/backend-category-helpers.d.ts +0 -1
- package/dist/lib/codex-manager/backend-category-prompt.d.ts +0 -1
- package/dist/lib/codex-manager/backend-settings-controller.d.ts +0 -1
- package/dist/lib/codex-manager/backend-settings-entry.d.ts +0 -1
- package/dist/lib/codex-manager/backend-settings-helpers.d.ts +0 -1
- package/dist/lib/codex-manager/backend-settings-prompt.d.ts +0 -1
- package/dist/lib/codex-manager/backend-settings-schema.d.ts +0 -1
- package/dist/lib/codex-manager/behavior-settings-panel.d.ts +0 -1
- package/dist/lib/codex-manager/commands/account.d.ts +8 -3
- package/dist/lib/codex-manager/commands/account.js +46 -34
- package/dist/lib/codex-manager/commands/best.d.ts +0 -1
- package/dist/lib/codex-manager/commands/bridge.d.ts +0 -1
- package/dist/lib/codex-manager/commands/budget.d.ts +8 -3
- package/dist/lib/codex-manager/commands/budget.js +6 -4
- package/dist/lib/codex-manager/commands/check.d.ts +0 -1
- package/dist/lib/codex-manager/commands/config-explain.d.ts +0 -1
- package/dist/lib/codex-manager/commands/debug-bundle.d.ts +0 -1
- package/dist/lib/codex-manager/commands/forecast.d.ts +1 -2
- package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/forecast.js +2 -2
- package/dist/lib/codex-manager/commands/history.d.ts +0 -1
- package/dist/lib/codex-manager/commands/init-config.d.ts +0 -1
- package/dist/lib/codex-manager/commands/integrations.d.ts +0 -1
- package/dist/lib/codex-manager/commands/limits.d.ts +0 -1
- package/dist/lib/codex-manager/commands/models.d.ts +0 -1
- package/dist/lib/codex-manager/commands/monitor.d.ts +0 -1
- package/dist/lib/codex-manager/commands/report.d.ts +0 -1
- package/dist/lib/codex-manager/commands/resets.d.ts +0 -1
- package/dist/lib/codex-manager/commands/resets.js +9 -1
- package/dist/lib/codex-manager/commands/rotation.d.ts +0 -1
- package/dist/lib/codex-manager/commands/status.d.ts +0 -1
- package/dist/lib/codex-manager/commands/switch.d.ts +0 -1
- package/dist/lib/codex-manager/commands/uninstall.d.ts +0 -1
- package/dist/lib/codex-manager/commands/unpin.d.ts +0 -1
- package/dist/lib/codex-manager/commands/usage.d.ts +0 -1
- package/dist/lib/codex-manager/commands/verify.d.ts +0 -1
- package/dist/lib/codex-manager/commands/why-selected.d.ts +0 -1
- package/dist/lib/codex-manager/commands/workspace.d.ts +0 -1
- package/dist/lib/codex-manager/dashboard-display-panel.d.ts +0 -1
- package/dist/lib/codex-manager/dashboard-formatters.d.ts +0 -1
- package/dist/lib/codex-manager/dashboard-settings-controller.d.ts +0 -1
- package/dist/lib/codex-manager/dashboard-settings-data.d.ts +0 -1
- package/dist/lib/codex-manager/dashboard-settings-entry.d.ts +0 -1
- package/dist/lib/codex-manager/experimental-settings-entry.d.ts +0 -1
- package/dist/lib/codex-manager/experimental-settings-prompt.d.ts +0 -1
- package/dist/lib/codex-manager/experimental-settings-schema.d.ts +0 -1
- package/dist/lib/codex-manager/experimental-sync-target-entry.d.ts +0 -1
- package/dist/lib/codex-manager/experimental-sync-target.d.ts +0 -1
- package/dist/lib/codex-manager/forecast-report-shared.d.ts +0 -1
- package/dist/lib/codex-manager/formatters/account-formatters.d.ts +0 -1
- package/dist/lib/codex-manager/formatters/index.d.ts +0 -1
- package/dist/lib/codex-manager/formatters/model-formatters.d.ts +0 -1
- package/dist/lib/codex-manager/formatters/quota-formatters.d.ts +0 -1
- package/dist/lib/codex-manager/formatters/text-style.d.ts +0 -1
- package/dist/lib/codex-manager/health-check.d.ts +0 -1
- package/dist/lib/codex-manager/health-check.js +1 -1
- package/dist/lib/codex-manager/help.d.ts +0 -1
- package/dist/lib/codex-manager/login-action-panel.d.ts +0 -1
- package/dist/lib/codex-manager/login-flow.d.ts +0 -1
- package/dist/lib/codex-manager/login-menu-actions.d.ts +0 -1
- package/dist/lib/codex-manager/login-menu-data.d.ts +0 -1
- package/dist/lib/codex-manager/login-menu-data.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-menu-data.js +7 -3
- package/dist/lib/codex-manager/login-oauth.d.ts +0 -1
- package/dist/lib/codex-manager/login-workspace-choice.d.ts +0 -1
- package/dist/lib/codex-manager/manual-callback.d.ts +0 -1
- package/dist/lib/codex-manager/persist-selected-account.d.ts +0 -1
- package/dist/lib/codex-manager/quota-cache-helpers.d.ts +7 -2
- package/dist/lib/codex-manager/quota-cache-helpers.d.ts.map +1 -1
- package/dist/lib/codex-manager/quota-cache-helpers.js +34 -19
- package/dist/lib/codex-manager/rate-limit-markers.d.ts +0 -1
- package/dist/lib/codex-manager/repair-commands.d.ts +0 -1
- package/dist/lib/codex-manager/repair-commands.js +1 -1
- package/dist/lib/codex-manager/settings-hub/backend.d.ts +0 -1
- package/dist/lib/codex-manager/settings-hub/dashboard.d.ts +0 -1
- package/dist/lib/codex-manager/settings-hub/experimental.d.ts +0 -1
- package/dist/lib/codex-manager/settings-hub/index.d.ts +0 -1
- package/dist/lib/codex-manager/settings-hub/shared.d.ts +0 -1
- package/dist/lib/codex-manager/settings-hub-entry.d.ts +0 -1
- package/dist/lib/codex-manager/settings-hub-menu.d.ts +0 -1
- package/dist/lib/codex-manager/settings-hub-prompt.d.ts +0 -1
- package/dist/lib/codex-manager/settings-hub.d.ts +0 -1
- package/dist/lib/codex-manager/settings-panels.d.ts +0 -1
- package/dist/lib/codex-manager/settings-persist-utils.d.ts +0 -1
- package/dist/lib/codex-manager/settings-preview.d.ts +0 -1
- package/dist/lib/codex-manager/settings-write-queue.d.ts +0 -1
- package/dist/lib/codex-manager/statusline-settings-panel.d.ts +0 -1
- package/dist/lib/codex-manager/theme-settings-panel.d.ts +0 -1
- package/dist/lib/codex-manager/unified-settings-controller.d.ts +0 -1
- package/dist/lib/codex-manager/unified-settings-entry.d.ts +0 -1
- package/dist/lib/codex-manager.d.ts +1 -2
- package/dist/lib/codex-manager.js +5 -95
- package/dist/lib/concurrency.d.ts +0 -1
- package/dist/lib/config-schema.d.ts +0 -1
- package/dist/lib/config.d.ts +0 -1
- package/dist/lib/config.js +46 -241
- package/dist/lib/constants.d.ts +0 -1
- package/dist/lib/context-budget/model-context-windows.d.ts +0 -1
- package/dist/lib/context-budget-guard.d.ts +0 -1
- package/dist/lib/context-budget-response.d.ts +0 -1
- package/dist/lib/context-budget-response.js +8 -8
- package/dist/lib/context-overflow.d.ts +0 -1
- package/dist/lib/context-overflow.js +10 -10
- package/dist/lib/dashboard-settings.d.ts +0 -1
- package/dist/lib/entitlement-cache.d.ts +0 -1
- package/dist/lib/env-parsing.d.ts +13 -1
- package/dist/lib/env-parsing.js +21 -0
- package/dist/lib/errors.d.ts +0 -1
- package/dist/lib/forecast.d.ts +0 -1
- package/dist/lib/fs-retry.d.ts +0 -1
- package/dist/lib/health.d.ts +0 -1
- package/dist/lib/index.d.ts +0 -1
- package/dist/lib/integration-generators.d.ts +0 -1
- package/dist/lib/live-account-sync.d.ts +0 -1
- package/dist/lib/local-bridge.d.ts +0 -1
- package/dist/lib/local-bridge.js +4 -1
- package/dist/lib/local-client-tokens.d.ts +0 -1
- package/dist/lib/local-client-tokens.js +125 -30
- package/dist/lib/logger.d.ts +0 -1
- package/dist/lib/logger.js +11 -1
- package/dist/lib/model-capability-matrix.d.ts +0 -1
- package/dist/lib/model-route-policy.d.ts +1 -2
- package/dist/lib/named-backup-export.d.ts +0 -1
- package/dist/lib/oauth-success.html +125 -125
- package/dist/lib/oc-chatgpt-import-adapter.d.ts +0 -1
- package/dist/lib/oc-chatgpt-orchestrator.d.ts +0 -1
- package/dist/lib/oc-chatgpt-target-detection.d.ts +0 -1
- package/dist/lib/parallel-probe.d.ts +0 -1
- package/dist/lib/policy/runtime-policy-cache.d.ts +80 -0
- package/dist/lib/policy/runtime-policy-cache.js +310 -0
- package/dist/lib/policy/runtime-policy.d.ts +1 -2
- package/dist/lib/policy/runtime-policy.js +46 -19
- package/dist/lib/preemptive-quota-scheduler.d.ts +0 -1
- package/dist/lib/proactive-refresh.d.ts +0 -1
- package/dist/lib/prompts/codex-host-bridge.d.ts +0 -1
- package/dist/lib/prompts/codex-host-bridge.js +138 -138
- package/dist/lib/prompts/codex.d.ts +0 -1
- package/dist/lib/prompts/codex.js +79 -79
- package/dist/lib/prompts/fetch-utils.d.ts +0 -1
- package/dist/lib/prompts/host-codex-prompt.d.ts +0 -1
- package/dist/lib/quota-cache.d.ts +7 -4
- package/dist/lib/quota-cache.d.ts.map +1 -1
- package/dist/lib/quota-cache.js +117 -61
- package/dist/lib/quota-probe.d.ts +0 -1
- package/dist/lib/quota-probe.js +5 -2
- package/dist/lib/quota-readiness.d.ts +7 -3
- package/dist/lib/quota-readiness.d.ts.map +1 -1
- package/dist/lib/quota-readiness.js +25 -4
- package/dist/lib/recovery/constants.d.ts +0 -1
- package/dist/lib/recovery/storage.d.ts +0 -1
- package/dist/lib/recovery/types.d.ts +0 -1
- package/dist/lib/recovery.d.ts +0 -1
- package/dist/lib/redaction.d.ts +0 -1
- package/dist/lib/refresh-guardian.d.ts +0 -1
- package/dist/lib/refresh-lease.d.ts +0 -1
- package/dist/lib/refresh-queue.d.ts +0 -1
- package/dist/lib/request/client-cancellation.d.ts +0 -1
- package/dist/lib/request/error-classification.d.ts +0 -1
- package/dist/lib/request/failover-config.d.ts +0 -1
- package/dist/lib/request/failure-policy.d.ts +0 -1
- package/dist/lib/request/fetch-helpers.d.ts +0 -1
- package/dist/lib/request/headers.d.ts +0 -1
- package/dist/lib/request/helpers/input-utils.d.ts +0 -1
- package/dist/lib/request/helpers/input-utils.js +88 -34
- package/dist/lib/request/helpers/model-map.d.ts +0 -1
- package/dist/lib/request/helpers/tool-utils.d.ts +0 -1
- package/dist/lib/request/helpers/tool-utils.js +421 -4
- package/dist/lib/request/index.d.ts +0 -1
- package/dist/lib/request/rate-limit-backoff.d.ts +0 -1
- package/dist/lib/request/rate-limit-decision.d.ts +0 -1
- package/dist/lib/request/request-attempt-budget.d.ts +0 -1
- package/dist/lib/request/request-init.d.ts +0 -1
- package/dist/lib/request/request-resilience.d.ts +0 -1
- package/dist/lib/request/request-transformer.d.ts +0 -1
- package/dist/lib/request/request-transformer.js +50 -25
- package/dist/lib/request/response-compaction.d.ts +0 -1
- package/dist/lib/request/response-handler.d.ts +0 -1
- package/dist/lib/request/response-metadata.d.ts +0 -1
- package/dist/lib/request/response-outcome.d.ts +0 -1
- package/dist/lib/request/stream-failover-runtime.d.ts +0 -1
- package/dist/lib/request/stream-failover.d.ts +0 -1
- package/dist/lib/request/stream-failover.js +28 -4
- package/dist/lib/request/token-refresh.d.ts +0 -1
- package/dist/lib/request/url-rewriting.d.ts +0 -1
- package/dist/lib/request/wait-utils.d.ts +0 -1
- package/dist/lib/rotation.d.ts +0 -1
- package/dist/lib/routing-mutex.d.ts +0 -1
- package/dist/lib/routing-profiles.d.ts +16 -2
- package/dist/lib/routing-profiles.js +119 -44
- package/dist/lib/runtime/account-check-helpers.d.ts +0 -1
- package/dist/lib/runtime/account-check-types.d.ts +0 -1
- package/dist/lib/runtime/account-check.d.ts +0 -1
- package/dist/lib/runtime/account-manager-cache-entry.d.ts +0 -1
- package/dist/lib/runtime/account-manager-cache.d.ts +0 -1
- package/dist/lib/runtime/account-model-catalog.d.ts +1 -2
- package/dist/lib/runtime/account-pool.d.ts +0 -1
- package/dist/lib/runtime/account-reset-credits.d.ts +0 -1
- package/dist/lib/runtime/account-reset-credits.js +8 -2
- package/dist/lib/runtime/account-scope.d.ts +0 -1
- package/dist/lib/runtime/account-select-event.d.ts +0 -1
- package/dist/lib/runtime/account-selection.d.ts +0 -1
- package/dist/lib/runtime/account-state.d.ts +0 -1
- package/dist/lib/runtime/account-status.d.ts +0 -1
- package/dist/lib/runtime/account-storage-scope-entry.d.ts +0 -1
- package/dist/lib/runtime/api-model-capabilities.d.ts +0 -1
- package/dist/lib/runtime/api-model-runtime.d.ts +0 -1
- package/dist/lib/runtime/app-bind.d.ts +0 -1
- package/dist/lib/runtime/app-bind.d.ts.map +1 -1
- package/dist/lib/runtime/app-bind.js +10 -2
- package/dist/lib/runtime/app-helper-selection.d.ts +0 -1
- package/dist/lib/runtime/auth-facade.d.ts +0 -1
- package/dist/lib/runtime/automatic-account-checks.d.ts +5 -2
- package/dist/lib/runtime/automatic-account-checks.d.ts.map +1 -1
- package/dist/lib/runtime/automatic-account-checks.js +29 -2
- package/dist/lib/runtime/automatic-subscription-checks.d.ts +1 -2
- package/dist/lib/runtime/automatic-subscription-checks.d.ts.map +1 -1
- package/dist/lib/runtime/automatic-subscription-checks.js +13 -16
- package/dist/lib/runtime/browser-oauth-flow.d.ts +0 -1
- package/dist/lib/runtime/capability-boost.d.ts +0 -1
- package/dist/lib/runtime/catalog-capabilities.d.ts +0 -1
- package/dist/lib/runtime/config-toml.d.ts +0 -1
- package/dist/lib/runtime/context-budget-settings.d.ts +0 -1
- package/dist/lib/runtime/event-handler.d.ts +0 -1
- package/dist/lib/runtime/first-run.d.ts +0 -1
- package/dist/lib/runtime/flagged-verify-types.d.ts +0 -1
- package/dist/lib/runtime/hydrate-emails.d.ts +0 -1
- package/dist/lib/runtime/inference-activity.d.ts +0 -1
- package/dist/lib/runtime/live-sync-entry.d.ts +0 -1
- package/dist/lib/runtime/live-sync.d.ts +0 -1
- package/dist/lib/runtime/loader-setup.d.ts +0 -1
- package/dist/lib/runtime/login-menu-accounts.d.ts +0 -1
- package/dist/lib/runtime/manual-oauth-flow.d.ts +0 -1
- package/dist/lib/runtime/model-discovery-status.d.ts +2 -3
- package/dist/lib/runtime/native-account-storage.d.ts +0 -1
- package/dist/lib/runtime/native-account-sync.d.ts +0 -1
- package/dist/lib/runtime/native-binding-lock.d.ts +0 -1
- package/dist/lib/runtime/native-client-auth.d.ts +0 -1
- package/dist/lib/runtime/native-provider-config.d.ts +0 -1
- package/dist/lib/runtime/native-rate-limits.d.ts +2 -1
- package/dist/lib/runtime/native-rate-limits.js +23 -6
- package/dist/lib/runtime/preemptive-quota.d.ts +0 -1
- package/dist/lib/runtime/quota-headers.d.ts +0 -1
- package/dist/lib/runtime/quota-probe.d.ts +0 -1
- package/dist/lib/runtime/quota-settings.d.ts +0 -1
- package/dist/lib/runtime/refresh-guardian-entry.d.ts +0 -1
- package/dist/lib/runtime/refresh-guardian.d.ts +0 -1
- package/dist/lib/runtime/request-init.d.ts +0 -1
- package/dist/lib/runtime/reset-credit-routing.d.ts +0 -1
- package/dist/lib/runtime/reset-credits.d.ts +0 -1
- package/dist/lib/runtime/response-output-history.d.ts +0 -1
- package/dist/lib/runtime/responses-websocket.d.ts +0 -1
- package/dist/lib/runtime/resume-picker.d.ts +0 -1
- package/dist/lib/runtime/rotation-account-selection.d.ts +0 -1
- package/dist/lib/runtime/rotation-proxy-state.d.ts +0 -1
- package/dist/lib/runtime/rotation-server-types.d.ts +0 -1
- package/dist/lib/runtime/rotation-storage-meta.d.ts +0 -1
- package/dist/lib/runtime/rotation-token-refresh.d.ts +0 -1
- package/dist/lib/runtime/runtime-capability-failures.d.ts +0 -1
- package/dist/lib/runtime/runtime-current-account.d.ts +0 -1
- package/dist/lib/runtime/runtime-observability.d.ts +0 -1
- package/dist/lib/runtime/runtime-services.d.ts +0 -1
- package/dist/lib/runtime/session-affinity-entry.d.ts +0 -1
- package/dist/lib/runtime/session-recovery.d.ts +0 -1
- package/dist/lib/runtime/status-marker.d.ts +0 -1
- package/dist/lib/runtime/storage-scope.d.ts +0 -1
- package/dist/lib/runtime/subscription-first-use.d.ts +0 -1
- package/dist/lib/runtime/subscription-quota-event.d.ts +0 -1
- package/dist/lib/runtime/subscription-quota-order.d.ts +0 -1
- package/dist/lib/runtime/toast.d.ts +0 -1
- package/dist/lib/runtime/ui-runtime-entry.d.ts +0 -1
- package/dist/lib/runtime/ui-runtime.d.ts +0 -1
- package/dist/lib/runtime/verify-flagged.d.ts +0 -1
- package/dist/lib/runtime/workspace-model-scopes.d.ts +0 -1
- package/dist/lib/runtime-constants.d.ts +0 -1
- package/dist/lib/runtime-paths.d.ts +0 -1
- package/dist/lib/runtime-rotation-proxy.d.ts +1 -1
- package/dist/lib/runtime-rotation-proxy.js +136 -3
- package/dist/lib/schemas.d.ts +35 -13
- package/dist/lib/schemas.js +19 -2
- package/dist/lib/session-affinity.d.ts +0 -1
- package/dist/lib/shutdown.d.ts +0 -1
- package/dist/lib/storage/account-clear-entry.d.ts +0 -1
- package/dist/lib/storage/account-clear.d.ts +0 -1
- package/dist/lib/storage/account-match-utils.d.ts +0 -1
- package/dist/lib/storage/account-persistence.d.ts +0 -1
- package/dist/lib/storage/account-port.d.ts +0 -1
- package/dist/lib/storage/account-save-entry.d.ts +0 -1
- package/dist/lib/storage/account-save.d.ts +9 -2
- package/dist/lib/storage/account-save.js +4 -1
- package/dist/lib/storage/account-snapshot.d.ts +0 -1
- package/dist/lib/storage/backup-metadata-builder.d.ts +0 -1
- package/dist/lib/storage/backup-metadata.d.ts +0 -1
- package/dist/lib/storage/backup-paths.d.ts +0 -1
- package/dist/lib/storage/backup-restore.d.ts +0 -1
- package/dist/lib/storage/cache-artifacts.d.ts +0 -1
- package/dist/lib/storage/credential-sidecars.d.ts +0 -1
- package/dist/lib/storage/error-hints.d.ts +0 -1
- package/dist/lib/storage/file-lock.d.ts +0 -1
- package/dist/lib/storage/file-lock.js +33 -1
- package/dist/lib/storage/file-paths.d.ts +0 -1
- package/dist/lib/storage/fixture-guards.d.ts +0 -1
- package/dist/lib/storage/flagged-entry.d.ts +0 -1
- package/dist/lib/storage/flagged-load-entry.d.ts +0 -1
- package/dist/lib/storage/flagged-save-entry.d.ts +0 -1
- package/dist/lib/storage/flagged-storage-file.d.ts +0 -1
- package/dist/lib/storage/flagged-storage-io.d.ts +0 -1
- package/dist/lib/storage/flagged-storage.d.ts +0 -1
- package/dist/lib/storage/gitignore.d.ts +0 -1
- package/dist/lib/storage/hash.d.ts +0 -1
- package/dist/lib/storage/health.d.ts +0 -1
- package/dist/lib/storage/identity.d.ts +0 -1
- package/dist/lib/storage/import-export.d.ts +0 -1
- package/dist/lib/storage/json-store-lock.d.ts +105 -0
- package/dist/lib/storage/json-store-lock.js +259 -0
- package/dist/lib/storage/metadata-section.d.ts +0 -1
- package/dist/lib/storage/migrations.d.ts +0 -1
- package/dist/lib/storage/migrations.js +31 -18
- package/dist/lib/storage/named-backup-entry.d.ts +0 -1
- package/dist/lib/storage/named-backups-entry.d.ts +0 -1
- package/dist/lib/storage/named-backups.d.ts +0 -1
- package/dist/lib/storage/path-state.d.ts +0 -1
- package/dist/lib/storage/paths.d.ts +0 -1
- package/dist/lib/storage/pending-auth.d.ts +10 -3
- package/dist/lib/storage/pending-auth.d.ts.map +1 -1
- package/dist/lib/storage/pending-auth.js +33 -9
- package/dist/lib/storage/project-migration.d.ts +0 -1
- package/dist/lib/storage/public-types.d.ts +0 -1
- package/dist/lib/storage/record-identity.d.ts +15 -0
- package/dist/lib/storage/record-identity.d.ts.map +1 -0
- package/dist/lib/storage/record-identity.js +16 -0
- package/dist/lib/storage/record-utils.d.ts +0 -1
- package/dist/lib/storage/restore-assessment.d.ts +0 -1
- package/dist/lib/storage/restore-backup-entry.d.ts +0 -1
- package/dist/lib/storage/restore-metadata.d.ts +0 -1
- package/dist/lib/storage/save-retry.d.ts +0 -1
- package/dist/lib/storage/snapshot-inspectors.d.ts +0 -1
- package/dist/lib/storage/snapshot-merge.d.ts +0 -1
- package/dist/lib/storage/storage-parser.d.ts +2 -1
- package/dist/lib/storage/storage-parser.js +10 -2
- package/dist/lib/storage/transactions.d.ts +0 -1
- package/dist/lib/storage.d.ts +15 -2
- package/dist/lib/storage.js +612 -25
- package/dist/lib/synthetic-response.d.ts +0 -1
- package/dist/lib/table-formatter.d.ts +0 -1
- package/dist/lib/temp-path.d.ts +0 -1
- package/dist/lib/tools/hashline-tools.d.ts +0 -1
- package/dist/lib/types.d.ts +0 -1
- package/dist/lib/ui/ansi.d.ts +0 -1
- package/dist/lib/ui/auth-menu-builder.d.ts +0 -1
- package/dist/lib/ui/auth-menu.d.ts +0 -1
- package/dist/lib/ui/check-progress.d.ts +0 -1
- package/dist/lib/ui/confirm.d.ts +0 -1
- package/dist/lib/ui/display-width.d.ts +0 -1
- package/dist/lib/ui/format.d.ts +0 -1
- package/dist/lib/ui/index.d.ts +0 -1
- package/dist/lib/ui/runtime.d.ts +0 -1
- package/dist/lib/ui/select.d.ts +0 -1
- package/dist/lib/ui/theme.d.ts +0 -1
- package/dist/lib/ui/ui-copy.d.ts +0 -1
- package/dist/lib/unified-settings.d.ts +13 -11
- package/dist/lib/unified-settings.js +22 -14
- package/dist/lib/update-notice.d.ts +0 -1
- package/dist/lib/usage/index.d.ts +0 -1
- package/dist/lib/usage/ledger.d.ts +0 -1
- package/dist/lib/usage/pricing.d.ts +0 -1
- package/dist/lib/usage/redaction.d.ts +0 -1
- package/dist/lib/usage/stream-usage-deferral.d.ts +0 -1
- package/dist/lib/usage/types.d.ts +0 -1
- package/dist/lib/usage/usage-extraction.d.ts +0 -1
- package/dist/lib/utils.d.ts +21 -1
- package/dist/lib/utils.js +31 -0
- package/dist/lib/wsl.d.ts +0 -1
- package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
- package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
- package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
- package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
- package/node_modules/@codex-ai/plugin/package.json +9 -9
- package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
- package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
- package/node_modules/@codex-ai/sdk/package.json +8 -8
- package/package.json +206 -200
- package/scripts/codex-app-launcher.js +653 -653
- package/scripts/codex-app-router.js +373 -373
- package/scripts/codex-bin-resolver.js +332 -313
- package/scripts/codex-multi-auth.js +65 -59
- package/scripts/codex-routing.js +62 -62
- package/scripts/codex.js +6923 -6642
- package/scripts/install-codex-auth-utils.js +213 -213
- package/scripts/mcodex.js +367 -367
- package/scripts/postinstall.js +114 -114
- package/scripts/preuninstall.js +265 -265
- package/skills/codex-auth-setup/SKILL.md +73 -0
- package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
- package/vendor/codex-ai-plugin/dist/index.js +2 -2
- package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
- package/vendor/codex-ai-plugin/dist/tool.js +29 -29
- package/vendor/codex-ai-plugin/package.json +9 -9
- package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
- package/vendor/codex-ai-sdk/dist/index.js +2 -2
- package/vendor/codex-ai-sdk/package.json +8 -8
package/dist/lib/storage.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, promises as fs, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import { ACCOUNT_LIMITS } from "./constants.js";
|
|
4
|
+
import { parseIntegerEnv } from "./env-parsing.js";
|
|
4
5
|
import { StorageError } from "./errors.js";
|
|
5
6
|
import { withFileOperationRetry, withRetry } from "./fs-retry.js";
|
|
6
7
|
import { createLogger } from "./logger.js";
|
|
@@ -69,6 +70,68 @@ const BACKUP_COPY_MAX_ATTEMPTS = 5;
|
|
|
69
70
|
const BACKUP_COPY_BASE_DELAY_MS = 10;
|
|
70
71
|
let storageBackupEnabled = true;
|
|
71
72
|
let lastAccountsSaveTimestamp = 0;
|
|
73
|
+
/**
|
|
74
|
+
* Default minimum age of the newest rotating `.bak` snapshot before a save
|
|
75
|
+
* pays for another full 3-slot rotation. Rotating backups are crash-recovery
|
|
76
|
+
* insurance, not a changelog: inside the window the primary-write path skips
|
|
77
|
+
* ~6 fs ops and ~2x payload bytes of staged copies per save. A credential
|
|
78
|
+
* change (account count or refresh-token set) still forces a fresh slot — a
|
|
79
|
+
* backup that predates an in-window rotation can only offer spent tokens — a
|
|
80
|
+
* missing `.bak` always rotates, and
|
|
81
|
+
* `CODEX_AUTH_STORAGE_BACKUP_MIN_INTERVAL_MS=0` restores the legacy
|
|
82
|
+
* rotate-on-every-save behavior.
|
|
83
|
+
*/
|
|
84
|
+
const ACCOUNTS_BACKUP_MIN_INTERVAL_DEFAULT_MS = 30_000;
|
|
85
|
+
function backupRotationMinIntervalMs() {
|
|
86
|
+
const parsed = parseIntegerEnv(process.env.CODEX_AUTH_STORAGE_BACKUP_MIN_INTERVAL_MS);
|
|
87
|
+
if (parsed === undefined) {
|
|
88
|
+
return ACCOUNTS_BACKUP_MIN_INTERVAL_DEFAULT_MS;
|
|
89
|
+
}
|
|
90
|
+
return Math.max(0, parsed);
|
|
91
|
+
}
|
|
92
|
+
const lastKnownPrimaryState = new Map();
|
|
93
|
+
const baselinelessSaveWarnedPaths = new Set();
|
|
94
|
+
function notePrimaryState(path, stats, sha256) {
|
|
95
|
+
if (!stats && sha256 === undefined)
|
|
96
|
+
return;
|
|
97
|
+
const fingerprint = { sha256 };
|
|
98
|
+
if (stats) {
|
|
99
|
+
if (Number.isFinite(stats.mtimeMs))
|
|
100
|
+
fingerprint.mtimeMs = stats.mtimeMs;
|
|
101
|
+
if (Number.isFinite(stats.ctimeMs))
|
|
102
|
+
fingerprint.ctimeMs = stats.ctimeMs;
|
|
103
|
+
if (Number.isFinite(stats.size))
|
|
104
|
+
fingerprint.size = stats.size;
|
|
105
|
+
// ino can exceed 2^53 on some filesystems; a lossy value would compare
|
|
106
|
+
// equal for two different files, so only record exact integers.
|
|
107
|
+
if (Number.isSafeInteger(stats.ino))
|
|
108
|
+
fingerprint.ino = stats.ino;
|
|
109
|
+
}
|
|
110
|
+
if (fingerprint.mtimeMs === undefined &&
|
|
111
|
+
fingerprint.ctimeMs === undefined &&
|
|
112
|
+
fingerprint.size === undefined &&
|
|
113
|
+
fingerprint.ino === undefined &&
|
|
114
|
+
fingerprint.sha256 === undefined) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
lastKnownPrimaryState.set(path, fingerprint);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Metadata-only identity check, used when no content hash was recorded. A
|
|
121
|
+
* foreign write bumps ctime on POSIX even when the writer preserves mtime
|
|
122
|
+
* (utimes), and a rename-replacement changes ino — together they catch the
|
|
123
|
+
* equal-mtime collisions a mtime-only comparison missed.
|
|
124
|
+
*/
|
|
125
|
+
function primaryFingerprintMatches(last, stats) {
|
|
126
|
+
return (last.mtimeMs !== undefined &&
|
|
127
|
+
last.ctimeMs !== undefined &&
|
|
128
|
+
last.size !== undefined &&
|
|
129
|
+
last.ino !== undefined &&
|
|
130
|
+
last.mtimeMs === stats.mtimeMs &&
|
|
131
|
+
last.ctimeMs === stats.ctimeMs &&
|
|
132
|
+
last.size === stats.size &&
|
|
133
|
+
last.ino === stats.ino);
|
|
134
|
+
}
|
|
72
135
|
async function ensureGitignore(storagePath) {
|
|
73
136
|
const state = getStoragePathState();
|
|
74
137
|
if (!state.currentStoragePath)
|
|
@@ -201,6 +264,185 @@ async function createRotatingAccountsBackup(path) {
|
|
|
201
264
|
}
|
|
202
265
|
}
|
|
203
266
|
}
|
|
267
|
+
function credentialSignatureOf(accounts) {
|
|
268
|
+
const refreshTokens = new Set();
|
|
269
|
+
for (const account of accounts) {
|
|
270
|
+
if (isRecord(account) &&
|
|
271
|
+
typeof account.refreshToken === "string" &&
|
|
272
|
+
account.refreshToken.length > 0) {
|
|
273
|
+
refreshTokens.add(account.refreshToken);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { count: accounts.length, refreshTokens };
|
|
277
|
+
}
|
|
278
|
+
function sameCredentialSignature(a, b) {
|
|
279
|
+
if (a.count !== b.count || a.refreshTokens.size !== b.refreshTokens.size) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
for (const token of a.refreshTokens) {
|
|
283
|
+
if (!b.refreshTokens.has(token))
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Cheap credential-signature probe for the throttle trigger. Reads and parses
|
|
290
|
+
* only the top-level `accounts` shape — far cheaper than the staged copies a
|
|
291
|
+
* rotation costs, and it runs at most once per save inside the throttle
|
|
292
|
+
* window. Returns null when the signature cannot be determined so the caller
|
|
293
|
+
* rotates rather than skipping a snapshot it cannot rule out.
|
|
294
|
+
*/
|
|
295
|
+
async function readOnDiskCredentialSignature(path) {
|
|
296
|
+
try {
|
|
297
|
+
const parsed = JSON.parse(await fs.readFile(path, "utf-8"));
|
|
298
|
+
if (isRecord(parsed) && Array.isArray(parsed.accounts)) {
|
|
299
|
+
return credentialSignatureOf(parsed.accounts);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// Missing or unparsable primary: fall through to the null sentinel.
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Refresh tokens still present in a recovery source: rotating and discovered
|
|
309
|
+
* `.bak*` snapshots plus any WAL journal payload. Pending rotated credentials
|
|
310
|
+
* whose spent token a restore could still resurface must survive pruning, so
|
|
311
|
+
* recovering a throttle-stale backup self-heals to the newest credential via
|
|
312
|
+
* `applyPendingAuth` instead of reviving a token that was spent upstream.
|
|
313
|
+
* Best-effort: unreadable or unparseable sources contribute nothing.
|
|
314
|
+
*/
|
|
315
|
+
async function collectRestorableRefreshTokens(path) {
|
|
316
|
+
const tokens = [];
|
|
317
|
+
const collectContent = (raw) => {
|
|
318
|
+
let parsed;
|
|
319
|
+
try {
|
|
320
|
+
parsed = JSON.parse(raw);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.accounts))
|
|
326
|
+
return;
|
|
327
|
+
for (const account of parsed.accounts) {
|
|
328
|
+
if (isRecord(account) &&
|
|
329
|
+
typeof account.refreshToken === "string" &&
|
|
330
|
+
account.refreshToken.length > 0) {
|
|
331
|
+
tokens.push(account.refreshToken);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
for (const candidate of await getAccountsBackupRecoveryCandidatesWithDiscovery(path)) {
|
|
336
|
+
try {
|
|
337
|
+
collectContent(await fs.readFile(candidate, "utf-8"));
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// An unreadable candidate contributes nothing.
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const walEntry = JSON.parse(await fs.readFile(getAccountsWalPath(path), "utf-8"));
|
|
345
|
+
if (isRecord(walEntry) && typeof walEntry.content === "string") {
|
|
346
|
+
collectContent(walEntry.content);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// No usable WAL payload.
|
|
351
|
+
}
|
|
352
|
+
return tokens;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Gate around `createRotatingAccountsBackup` that throttles rotation churn.
|
|
356
|
+
* Rotates when the newest `.bak` is missing (always preserve at least one
|
|
357
|
+
* recovery snapshot), when it is older than `backupRotationMinIntervalMs()`,
|
|
358
|
+
* or when the incoming write changes the stored credential signature
|
|
359
|
+
* (account count or refresh-token set) — the case where the pre-write state
|
|
360
|
+
* is worth a fresh slot even inside the window.
|
|
361
|
+
* `expectedOnDiskAccounts` lets callers that already read the primary under
|
|
362
|
+
* the lock (the three-way merge path) skip the signature probe.
|
|
363
|
+
*/
|
|
364
|
+
async function rotateAccountsBackupIfDue(path, nextAccounts, expectedOnDiskAccounts) {
|
|
365
|
+
const latestBackupPath = getAccountsBackupPath(path);
|
|
366
|
+
let newestBackupAgeMs = null;
|
|
367
|
+
try {
|
|
368
|
+
newestBackupAgeMs = Date.now() - (await fs.stat(latestBackupPath)).mtimeMs;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// Missing or unreadable newest backup: rotate so >=1 snapshot exists.
|
|
372
|
+
newestBackupAgeMs = null;
|
|
373
|
+
}
|
|
374
|
+
const minIntervalMs = backupRotationMinIntervalMs();
|
|
375
|
+
// Age is clamped at 0: a backup whose mtime lands fractionally ahead of
|
|
376
|
+
// Date.now() (fs timestamp granularity vs. the clock) is "fresh", and the
|
|
377
|
+
// clamp also keeps `minIntervalMs = 0` meaning "always rotate".
|
|
378
|
+
if (newestBackupAgeMs !== null && Math.max(0, newestBackupAgeMs) < minIntervalMs) {
|
|
379
|
+
const onDiskSignature = expectedOnDiskAccounts !== undefined
|
|
380
|
+
? credentialSignatureOf(expectedOnDiskAccounts ?? [])
|
|
381
|
+
: await readOnDiskCredentialSignature(path);
|
|
382
|
+
if (onDiskSignature !== null &&
|
|
383
|
+
sameCredentialSignature(onDiskSignature, credentialSignatureOf(nextAccounts))) {
|
|
384
|
+
log.debug("Skipping account backup rotation inside throttle window", {
|
|
385
|
+
path,
|
|
386
|
+
backupPath: latestBackupPath,
|
|
387
|
+
newestBackupAgeMs,
|
|
388
|
+
minIntervalMs,
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
await createRotatingAccountsBackup(path);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Warn (once per storage path per process) when a save without a merge
|
|
397
|
+
* baseline is about to overwrite a primary this process did not last read or
|
|
398
|
+
* write — the cheap signature of "concurrent modification, no baseline"
|
|
399
|
+
* (last-writer-wins). The WeakMap baseline in `saveAccounts` cannot attach to
|
|
400
|
+
* raw `structuredClone` copies, so this is the residual guard for untracked
|
|
401
|
+
* objects. First-ever saves (no primary), empty primaries, and files whose
|
|
402
|
+
* content (or full stat tuple, when no hash was recorded) matches our last
|
|
403
|
+
* read/write stay quiet; the write is never blocked.
|
|
404
|
+
*/
|
|
405
|
+
async function warnOnBaselinelessSave(path) {
|
|
406
|
+
if (baselinelessSaveWarnedPaths.has(path))
|
|
407
|
+
return;
|
|
408
|
+
// No primary on disk (or an unreadable one): nothing exists to silently
|
|
409
|
+
// lose, so the first-ever save stays quiet.
|
|
410
|
+
const stats = await fs.stat(path).catch(() => undefined);
|
|
411
|
+
if (!stats?.isFile() || stats.size === 0)
|
|
412
|
+
return;
|
|
413
|
+
const last = lastKnownPrimaryState.get(path);
|
|
414
|
+
if (last !== undefined) {
|
|
415
|
+
if (last.sha256 !== undefined) {
|
|
416
|
+
// Content-exact check: identical bytes mean nothing was lost to a
|
|
417
|
+
// foreign write regardless of timestamp granularity or an mtime-
|
|
418
|
+
// preserving writer, and different bytes mean one landed even when
|
|
419
|
+
// every stat field coincidentally matches.
|
|
420
|
+
const currentSha256 = await fs
|
|
421
|
+
.readFile(path, "utf-8")
|
|
422
|
+
.then((raw) => computeSha256(raw))
|
|
423
|
+
.catch(() => undefined);
|
|
424
|
+
if (currentSha256 !== undefined) {
|
|
425
|
+
if (currentSha256 === last.sha256)
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
else if (primaryFingerprintMatches(last, stats)) {
|
|
429
|
+
// Content unreadable mid-check: fall back to the metadata tuple.
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else if (primaryFingerprintMatches(last, stats)) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
baselinelessSaveWarnedPaths.add(path);
|
|
438
|
+
const message = "Saving account storage without a merge baseline over a primary file this process did not last read or write; a concurrent writer's changes may be overwritten (last-writer-wins). Load via loadAccounts() or clone via cloneTrackedAccountStorage() so the three-way merge can run.";
|
|
439
|
+
log.warn("Baseline-less account save over externally modified primary", {
|
|
440
|
+
path,
|
|
441
|
+
});
|
|
442
|
+
process.emitWarning(message, {
|
|
443
|
+
code: "CODEX_MULTI_AUTH_BASELINELESS_SAVE",
|
|
444
|
+
});
|
|
445
|
+
}
|
|
204
446
|
function isRotatingBackupTempArtifact(storagePath, candidatePath) {
|
|
205
447
|
const backupPrefix = `${storagePath}${ACCOUNTS_BACKUP_SUFFIX}`;
|
|
206
448
|
if (!candidatePath.startsWith(backupPrefix) ||
|
|
@@ -467,7 +709,14 @@ export async function restoreAccountsFromBackup(path, options) {
|
|
|
467
709
|
normalizeAccountStorage,
|
|
468
710
|
isRecord,
|
|
469
711
|
}),
|
|
470
|
-
saveAccounts
|
|
712
|
+
saveAccounts: async (storage) => {
|
|
713
|
+
// An explicit restore is a deliberate overwrite of the current
|
|
714
|
+
// primary, like the crash-recovery persists: record the file being
|
|
715
|
+
// replaced so the baseline-less-save guard neither misreports it as
|
|
716
|
+
// a foreign write nor spends the once-per-path warning on it.
|
|
717
|
+
notePrimaryState(getStoragePath(), await fs.stat(getStoragePath()).catch(() => undefined));
|
|
718
|
+
await saveAccounts(storage);
|
|
719
|
+
},
|
|
471
720
|
});
|
|
472
721
|
}
|
|
473
722
|
export async function exportNamedBackup(name, options) {
|
|
@@ -508,15 +757,31 @@ async function migrateLegacyProjectStorageIfNeeded(options) {
|
|
|
508
757
|
if (existingCandidatePaths.length === 0) {
|
|
509
758
|
return null;
|
|
510
759
|
}
|
|
511
|
-
const loadCurrentStorageForMigration = async () =>
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
760
|
+
const loadCurrentStorageForMigration = async () => {
|
|
761
|
+
// Stat before the read so a migration persist of the merged content
|
|
762
|
+
// counts as a state this process knows (baseline-less-save guard).
|
|
763
|
+
// Recorded even when the parse fails: a migration persist that
|
|
764
|
+
// replaces an unreadable current file is a deliberate overwrite, not
|
|
765
|
+
// a foreign modification to warn about.
|
|
766
|
+
let stats;
|
|
767
|
+
try {
|
|
768
|
+
stats = await fs.stat(currentStoragePath);
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
stats = undefined;
|
|
772
|
+
}
|
|
773
|
+
const storage = await loadNormalizedStorageFromPath(currentStoragePath, "current account storage", {
|
|
774
|
+
loadAccountsFromPath: (path) => loadAccountsFromPath(path, {
|
|
775
|
+
normalizeAccountStorage,
|
|
776
|
+
isRecord,
|
|
777
|
+
}),
|
|
778
|
+
logWarn: (message, details) => {
|
|
779
|
+
log.warn(message, details);
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
notePrimaryState(currentStoragePath, stats);
|
|
783
|
+
return storage;
|
|
784
|
+
};
|
|
520
785
|
const readLiveCurrentStorageIfExportMode = async () => {
|
|
521
786
|
if (commit || !existsSync(currentStoragePath)) {
|
|
522
787
|
return { exists: false, storage: null };
|
|
@@ -755,17 +1020,289 @@ export function resolveAccountSelectionIndex(accounts, candidate, fallbackIndex
|
|
|
755
1020
|
return matchedIndex;
|
|
756
1021
|
return clampIndex(fallbackIndex, accounts.length);
|
|
757
1022
|
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Total "newest" order over surviving entries, identical to folding
|
|
1025
|
+
* selectNewestAccount left-to-right over the survivor array: higher
|
|
1026
|
+
* `lastUsed` wins, then higher `addedAt`, then — the fold's behavior on a
|
|
1027
|
+
* full tie — the later array position. Reducing the fold to one comparison
|
|
1028
|
+
* over (lastUsed, addedAt, index) is what lets each identity lookup be
|
|
1029
|
+
* answered from an incrementally maintained index instead of a linear scan.
|
|
1030
|
+
*/
|
|
1031
|
+
function compareDedupIndexEntries(a, b) {
|
|
1032
|
+
if (a.lastUsed !== b.lastUsed)
|
|
1033
|
+
return a.lastUsed - b.lastUsed;
|
|
1034
|
+
if (a.addedAt !== b.addedAt)
|
|
1035
|
+
return a.addedAt - b.addedAt;
|
|
1036
|
+
return a.index - b.index;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Incremental "newest surviving owner" index for one identity facet value
|
|
1040
|
+
* (a composite accountId+email pair, an email key, or a refresh token).
|
|
1041
|
+
* Survivor slots join when appended, refresh their freshness when a
|
|
1042
|
+
* newest-wins merge replaces the account stored in the slot, and leave when
|
|
1043
|
+
* a replacement drops the facet. A max-heap with lazy deletion answers the
|
|
1044
|
+
* argmax query in amortized O(log n): stale heap entries are popped on
|
|
1045
|
+
* first sight, so every join/update/leave is paid for exactly once.
|
|
1046
|
+
*/
|
|
1047
|
+
class DedupNewestIndex {
|
|
1048
|
+
liveEntries = new Map();
|
|
1049
|
+
heap = [];
|
|
1050
|
+
get size() {
|
|
1051
|
+
return this.liveEntries.size;
|
|
1052
|
+
}
|
|
1053
|
+
set(index, account) {
|
|
1054
|
+
const entry = {
|
|
1055
|
+
index,
|
|
1056
|
+
lastUsed: account.lastUsed || 0,
|
|
1057
|
+
addedAt: account.addedAt || 0,
|
|
1058
|
+
};
|
|
1059
|
+
this.liveEntries.set(index, entry);
|
|
1060
|
+
const heap = this.heap;
|
|
1061
|
+
let slot = heap.length;
|
|
1062
|
+
heap.push(entry);
|
|
1063
|
+
while (slot > 0) {
|
|
1064
|
+
const parent = (slot - 1) >> 1;
|
|
1065
|
+
const parentEntry = heap[parent];
|
|
1066
|
+
if (parentEntry === undefined ||
|
|
1067
|
+
compareDedupIndexEntries(parentEntry, entry) >= 0) {
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
heap[slot] = parentEntry;
|
|
1071
|
+
slot = parent;
|
|
1072
|
+
}
|
|
1073
|
+
heap[slot] = entry;
|
|
1074
|
+
}
|
|
1075
|
+
delete(index) {
|
|
1076
|
+
this.liveEntries.delete(index);
|
|
1077
|
+
}
|
|
1078
|
+
newestIndex() {
|
|
1079
|
+
const heap = this.heap;
|
|
1080
|
+
while (heap.length > 0) {
|
|
1081
|
+
const top = heap[0];
|
|
1082
|
+
if (top !== undefined && this.liveEntries.get(top.index) === top) {
|
|
1083
|
+
return top.index;
|
|
1084
|
+
}
|
|
1085
|
+
this.dropHeapTop();
|
|
1086
|
+
}
|
|
1087
|
+
return undefined;
|
|
1088
|
+
}
|
|
1089
|
+
dropHeapTop() {
|
|
1090
|
+
const heap = this.heap;
|
|
1091
|
+
const last = heap.pop();
|
|
1092
|
+
if (last === undefined || heap.length === 0)
|
|
1093
|
+
return;
|
|
1094
|
+
heap[0] = last;
|
|
1095
|
+
let slot = 0;
|
|
1096
|
+
for (;;) {
|
|
1097
|
+
const left = slot * 2 + 1;
|
|
1098
|
+
const right = left + 1;
|
|
1099
|
+
let best = slot;
|
|
1100
|
+
let bestEntry = heap[best] ?? last;
|
|
1101
|
+
const leftEntry = left < heap.length ? heap[left] : undefined;
|
|
1102
|
+
if (leftEntry !== undefined &&
|
|
1103
|
+
compareDedupIndexEntries(leftEntry, bestEntry) > 0) {
|
|
1104
|
+
best = left;
|
|
1105
|
+
bestEntry = leftEntry;
|
|
1106
|
+
}
|
|
1107
|
+
const rightEntry = right < heap.length ? heap[right] : undefined;
|
|
1108
|
+
if (rightEntry !== undefined &&
|
|
1109
|
+
compareDedupIndexEntries(rightEntry, bestEntry) > 0) {
|
|
1110
|
+
best = right;
|
|
1111
|
+
}
|
|
1112
|
+
if (best === slot)
|
|
1113
|
+
break;
|
|
1114
|
+
const slotEntry = heap[slot];
|
|
1115
|
+
const bestValue = heap[best];
|
|
1116
|
+
if (bestValue !== undefined)
|
|
1117
|
+
heap[slot] = bestValue;
|
|
1118
|
+
if (slotEntry !== undefined)
|
|
1119
|
+
heap[best] = slotEntry;
|
|
1120
|
+
slot = best;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
function bumpIdentityCount(counts, key, delta) {
|
|
1125
|
+
if (!key)
|
|
1126
|
+
return;
|
|
1127
|
+
const next = (counts.get(key) ?? 0) + delta;
|
|
1128
|
+
if (next <= 0) {
|
|
1129
|
+
counts.delete(key);
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
counts.set(key, next);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function dedupCompositeKey(ref) {
|
|
1136
|
+
if (!ref.accountId || !ref.emailKey)
|
|
1137
|
+
return undefined;
|
|
1138
|
+
// JSON encoding keeps the pair unambiguous even for adversarial strings.
|
|
1139
|
+
return JSON.stringify([ref.accountId, ref.emailKey]);
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* One dedup pass in O(n log n): every candidate is matched against the
|
|
1143
|
+
* surviving array through four incrementally maintained indexes (composite
|
|
1144
|
+
* accountId+email, email, refresh token, accountId) that apply the same
|
|
1145
|
+
* tier precedence and vetoes as the linear findMatchingAccountIndex scans
|
|
1146
|
+
* they replace. A newest-wins replacement re-keys every index the loser
|
|
1147
|
+
* owned, so the indexes always describe the current survivor array.
|
|
1148
|
+
*/
|
|
758
1149
|
function deduplicateAccountsByIdentityPass(accounts) {
|
|
759
1150
|
const deduplicated = [];
|
|
1151
|
+
const refs = [];
|
|
1152
|
+
const compositeIndexes = new Map();
|
|
1153
|
+
const emailIndexes = new Map();
|
|
1154
|
+
const refreshIndexes = new Map();
|
|
1155
|
+
const accountIdOwners = new Map();
|
|
1156
|
+
const addToIndexes = (index, account, ref) => {
|
|
1157
|
+
const compositeKey = dedupCompositeKey(ref);
|
|
1158
|
+
if (compositeKey !== undefined) {
|
|
1159
|
+
let newest = compositeIndexes.get(compositeKey);
|
|
1160
|
+
if (!newest) {
|
|
1161
|
+
newest = new DedupNewestIndex();
|
|
1162
|
+
compositeIndexes.set(compositeKey, newest);
|
|
1163
|
+
}
|
|
1164
|
+
newest.set(index, account);
|
|
1165
|
+
}
|
|
1166
|
+
if (ref.emailKey !== undefined) {
|
|
1167
|
+
let bucket = emailIndexes.get(ref.emailKey);
|
|
1168
|
+
if (!bucket) {
|
|
1169
|
+
bucket = { newest: new DedupNewestIndex(), accountIds: new Map() };
|
|
1170
|
+
emailIndexes.set(ref.emailKey, bucket);
|
|
1171
|
+
}
|
|
1172
|
+
bucket.newest.set(index, account);
|
|
1173
|
+
bumpIdentityCount(bucket.accountIds, ref.accountId, 1);
|
|
1174
|
+
}
|
|
1175
|
+
if (ref.refreshToken !== undefined) {
|
|
1176
|
+
let bucket = refreshIndexes.get(ref.refreshToken);
|
|
1177
|
+
if (!bucket) {
|
|
1178
|
+
bucket = {
|
|
1179
|
+
newest: new DedupNewestIndex(),
|
|
1180
|
+
accountIds: new Map(),
|
|
1181
|
+
emailKeys: new Map(),
|
|
1182
|
+
};
|
|
1183
|
+
refreshIndexes.set(ref.refreshToken, bucket);
|
|
1184
|
+
}
|
|
1185
|
+
bucket.newest.set(index, account);
|
|
1186
|
+
bumpIdentityCount(bucket.accountIds, ref.accountId, 1);
|
|
1187
|
+
bumpIdentityCount(bucket.emailKeys, ref.emailKey, 1);
|
|
1188
|
+
}
|
|
1189
|
+
if (ref.accountId !== undefined) {
|
|
1190
|
+
let owners = accountIdOwners.get(ref.accountId);
|
|
1191
|
+
if (!owners) {
|
|
1192
|
+
owners = new Set();
|
|
1193
|
+
accountIdOwners.set(ref.accountId, owners);
|
|
1194
|
+
}
|
|
1195
|
+
owners.add(index);
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
const removeFromIndexes = (index, ref) => {
|
|
1199
|
+
const compositeKey = dedupCompositeKey(ref);
|
|
1200
|
+
if (compositeKey !== undefined) {
|
|
1201
|
+
compositeIndexes.get(compositeKey)?.delete(index);
|
|
1202
|
+
}
|
|
1203
|
+
if (ref.emailKey !== undefined) {
|
|
1204
|
+
const bucket = emailIndexes.get(ref.emailKey);
|
|
1205
|
+
if (bucket) {
|
|
1206
|
+
bucket.newest.delete(index);
|
|
1207
|
+
bumpIdentityCount(bucket.accountIds, ref.accountId, -1);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (ref.refreshToken !== undefined) {
|
|
1211
|
+
const bucket = refreshIndexes.get(ref.refreshToken);
|
|
1212
|
+
if (bucket) {
|
|
1213
|
+
bucket.newest.delete(index);
|
|
1214
|
+
bumpIdentityCount(bucket.accountIds, ref.accountId, -1);
|
|
1215
|
+
bumpIdentityCount(bucket.emailKeys, ref.emailKey, -1);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (ref.accountId !== undefined) {
|
|
1219
|
+
accountIdOwners.get(ref.accountId)?.delete(index);
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
760
1222
|
for (const account of accounts) {
|
|
761
1223
|
if (!account)
|
|
762
1224
|
continue;
|
|
763
|
-
const
|
|
1225
|
+
const candidateRef = toAccountIdentityRef(account);
|
|
1226
|
+
let existingIndex;
|
|
1227
|
+
// Tier 1: composite accountId + email match.
|
|
1228
|
+
if (candidateRef.accountId && candidateRef.emailKey) {
|
|
1229
|
+
const compositeKey = dedupCompositeKey(candidateRef);
|
|
1230
|
+
if (compositeKey !== undefined) {
|
|
1231
|
+
existingIndex = compositeIndexes.get(compositeKey)?.newestIndex();
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
// Tier 2: safe email match. Refuse when the email's owners plus the
|
|
1235
|
+
// candidate carry more than one distinct accountId.
|
|
1236
|
+
if (existingIndex === undefined && candidateRef.emailKey) {
|
|
1237
|
+
const bucket = emailIndexes.get(candidateRef.emailKey);
|
|
1238
|
+
if (bucket && bucket.newest.size > 0) {
|
|
1239
|
+
const distinctAccountIds = bucket.accountIds.size +
|
|
1240
|
+
(candidateRef.accountId &&
|
|
1241
|
+
!bucket.accountIds.has(candidateRef.accountId)
|
|
1242
|
+
? 1
|
|
1243
|
+
: 0);
|
|
1244
|
+
if (distinctAccountIds <= 1) {
|
|
1245
|
+
existingIndex = bucket.newest.newestIndex();
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
// Tier 3: compatible refresh-token match. Refuse on an owner whose
|
|
1250
|
+
// accountId or emailKey conflicts with the candidate's, and on an
|
|
1251
|
+
// ambiguous refresh-only candidate with more than one owner.
|
|
1252
|
+
if (existingIndex === undefined && candidateRef.refreshToken) {
|
|
1253
|
+
const bucket = refreshIndexes.get(candidateRef.refreshToken);
|
|
1254
|
+
if (bucket && bucket.newest.size > 0) {
|
|
1255
|
+
const accountIdConflict = candidateRef.accountId !== undefined &&
|
|
1256
|
+
(bucket.accountIds.size > 1 ||
|
|
1257
|
+
(bucket.accountIds.size === 1 &&
|
|
1258
|
+
!bucket.accountIds.has(candidateRef.accountId)));
|
|
1259
|
+
const emailKeyConflict = candidateRef.emailKey !== undefined &&
|
|
1260
|
+
(bucket.emailKeys.size > 1 ||
|
|
1261
|
+
(bucket.emailKeys.size === 1 &&
|
|
1262
|
+
!bucket.emailKeys.has(candidateRef.emailKey)));
|
|
1263
|
+
const ambiguous = candidateRef.accountId === undefined &&
|
|
1264
|
+
candidateRef.emailKey === undefined &&
|
|
1265
|
+
bucket.newest.size > 1;
|
|
1266
|
+
if (!accountIdConflict && !emailKeyConflict && !ambiguous) {
|
|
1267
|
+
existingIndex = bucket.newest.newestIndex();
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
// Tier 4: unique-accountId fallback. The dedup pass runs
|
|
1272
|
+
// findMatchingAccountIndex with default options, so the fallback is
|
|
1273
|
+
// only reachable for candidates that also carry an email key; a sole
|
|
1274
|
+
// owner still loses on a conflicting email key.
|
|
1275
|
+
if (existingIndex === undefined &&
|
|
1276
|
+
candidateRef.accountId &&
|
|
1277
|
+
candidateRef.emailKey) {
|
|
1278
|
+
const owners = accountIdOwners.get(candidateRef.accountId);
|
|
1279
|
+
if (owners && owners.size === 1) {
|
|
1280
|
+
const only = owners.values().next();
|
|
1281
|
+
if (!only.done) {
|
|
1282
|
+
const ownerEmailKey = refs[only.value]?.emailKey;
|
|
1283
|
+
if (ownerEmailKey === undefined ||
|
|
1284
|
+
ownerEmailKey === candidateRef.emailKey) {
|
|
1285
|
+
existingIndex = only.value;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
764
1290
|
if (existingIndex === undefined) {
|
|
1291
|
+
const index = deduplicated.length;
|
|
765
1292
|
deduplicated.push(account);
|
|
1293
|
+
refs.push(candidateRef);
|
|
1294
|
+
addToIndexes(index, account, candidateRef);
|
|
766
1295
|
continue;
|
|
767
1296
|
}
|
|
768
|
-
|
|
1297
|
+
const current = deduplicated[existingIndex];
|
|
1298
|
+
if (selectNewestAccount(current, account) === current)
|
|
1299
|
+
continue;
|
|
1300
|
+
deduplicated[existingIndex] = account;
|
|
1301
|
+
const previousRef = refs[existingIndex];
|
|
1302
|
+
refs[existingIndex] = candidateRef;
|
|
1303
|
+
if (previousRef)
|
|
1304
|
+
removeFromIndexes(existingIndex, previousRef);
|
|
1305
|
+
addToIndexes(existingIndex, account, candidateRef);
|
|
769
1306
|
}
|
|
770
1307
|
return deduplicated;
|
|
771
1308
|
}
|
|
@@ -1266,11 +1803,25 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
1266
1803
|
const migratedLegacyStorage = persistMigration
|
|
1267
1804
|
? await migrateLegacyProjectStorageIfNeeded({ persist: persistMigration })
|
|
1268
1805
|
: null;
|
|
1806
|
+
// Stat BEFORE the primary read: this fingerprint is the state this process
|
|
1807
|
+
// can claim to know. A writer racing the read makes the record stale, which
|
|
1808
|
+
// is the safe direction for the baseline-less-save guard — never claim
|
|
1809
|
+
// knowledge of content we may not have parsed.
|
|
1810
|
+
let primaryStatsBeforeRead;
|
|
1811
|
+
try {
|
|
1812
|
+
primaryStatsBeforeRead = await fs.stat(path);
|
|
1813
|
+
}
|
|
1814
|
+
catch {
|
|
1815
|
+
primaryStatsBeforeRead = undefined;
|
|
1816
|
+
}
|
|
1269
1817
|
try {
|
|
1270
|
-
const { normalized, storedVersion, schemaErrors } = await loadAccountsFromPath(path, {
|
|
1818
|
+
const { normalized, storedVersion, schemaErrors, contentSha256 } = await loadAccountsFromPath(path, {
|
|
1271
1819
|
normalizeAccountStorage,
|
|
1272
1820
|
isRecord,
|
|
1273
1821
|
});
|
|
1822
|
+
if (normalized) {
|
|
1823
|
+
notePrimaryState(path, primaryStatsBeforeRead, contentSha256);
|
|
1824
|
+
}
|
|
1274
1825
|
if (schemaErrors.length > 0) {
|
|
1275
1826
|
log.warn("Account storage schema validation warnings", {
|
|
1276
1827
|
errors: schemaErrors.slice(0, 5),
|
|
@@ -1348,6 +1899,12 @@ async function loadAccountsInternal(persistMigration) {
|
|
|
1348
1899
|
return normalized;
|
|
1349
1900
|
}
|
|
1350
1901
|
catch (error) {
|
|
1902
|
+
// The primary could not be parsed (or read). Whatever file we stat'd is
|
|
1903
|
+
// the state the WAL/backup recovery persists below deliberately replace,
|
|
1904
|
+
// so record it as known — otherwise those intentional recovery saves
|
|
1905
|
+
// would emit the baseline-less-overwrite warning and spend the
|
|
1906
|
+
// once-per-path guard meant for genuine foreign writes.
|
|
1907
|
+
notePrimaryState(path, primaryStatsBeforeRead);
|
|
1351
1908
|
const code = error.code;
|
|
1352
1909
|
if (existsSync(resetMarkerPath)) {
|
|
1353
1910
|
return createEmptyStorageWithMetadata(false, "intentional-reset");
|
|
@@ -1467,14 +2024,17 @@ async function loadAccountsForExport() {
|
|
|
1467
2024
|
throw error;
|
|
1468
2025
|
}
|
|
1469
2026
|
}
|
|
1470
|
-
async function saveAccountsUnlocked(storage) {
|
|
1471
|
-
await saveAccountsUnlockedToDisk(storage);
|
|
1472
|
-
// A persisted (or superseded) pending rotated credential is no longer needed
|
|
1473
|
-
|
|
2027
|
+
async function saveAccountsUnlocked(storage, options) {
|
|
2028
|
+
await saveAccountsUnlockedToDisk(storage, options);
|
|
2029
|
+
// A persisted (or superseded) pending rotated credential is no longer needed —
|
|
2030
|
+
// unless a restorable backup/WAL snapshot still carries the token it replaces,
|
|
2031
|
+
// in which case the entry stays so a stale-backup recovery self-heals.
|
|
2032
|
+
const path = getStoragePath();
|
|
2033
|
+
await prunePendingAuth(path, storage, () => collectRestorableRefreshTokens(path)).catch((error) => {
|
|
1474
2034
|
log.warn("Failed to prune pending rotated credentials", { code: typeof error.code === "string" && /^[A-Z_]{1,40}$/.test(error.code ?? "") ? error.code : "INVALID_PENDING_AUTH" });
|
|
1475
2035
|
});
|
|
1476
2036
|
}
|
|
1477
|
-
async function saveAccountsUnlockedToDisk(storage) {
|
|
2037
|
+
async function saveAccountsUnlockedToDisk(storage, options) {
|
|
1478
2038
|
const path = getStoragePath();
|
|
1479
2039
|
const resetMarkerPath = getIntentionalResetMarkerPath(path);
|
|
1480
2040
|
const walPath = getAccountsWalPath(path);
|
|
@@ -1513,7 +2073,7 @@ async function saveAccountsUnlockedToDisk(storage) {
|
|
|
1513
2073
|
},
|
|
1514
2074
|
}),
|
|
1515
2075
|
createSyntheticFixtureError: () => new StorageError("Refusing to overwrite non-synthetic account storage with synthetic fixture payload", "EINVALID", path, "Detected synthetic fixture-like account payload. Use explicit account import/login commands instead."),
|
|
1516
|
-
createRotatingAccountsBackup,
|
|
2076
|
+
createRotatingAccountsBackup: (targetPath) => rotateAccountsBackupIfDue(targetPath, storage.accounts, options?.expectedOnDiskAccounts),
|
|
1517
2077
|
computeSha256,
|
|
1518
2078
|
writeJournal: async (content, journalPath) => {
|
|
1519
2079
|
const journalEntry = {
|
|
@@ -1571,8 +2131,9 @@ async function saveAccountsUnlockedToDisk(storage) {
|
|
|
1571
2131
|
// Ignore cleanup failure.
|
|
1572
2132
|
}
|
|
1573
2133
|
},
|
|
1574
|
-
onSaved: () => {
|
|
2134
|
+
onSaved: (stats, contentSha256) => {
|
|
1575
2135
|
lastAccountsSaveTimestamp = Date.now();
|
|
2136
|
+
notePrimaryState(path, stats, contentSha256);
|
|
1576
2137
|
},
|
|
1577
2138
|
logWarn: (message, details) => {
|
|
1578
2139
|
log.warn(message, details);
|
|
@@ -1648,14 +2209,18 @@ export async function withAccountAndFlaggedStorageTransaction(handler) {
|
|
|
1648
2209
|
const baseline = loadedAccountSnapshots.get(accountStorage);
|
|
1649
2210
|
const nextAccounts = cloneAccountStorageForPersistence(baseline ? mergeAccountSnapshot(baseline, state.snapshot, accountStorage) : accountStorage);
|
|
1650
2211
|
const nextFlagged = cloneFlaggedStorageForPersistence(flaggedStorage);
|
|
1651
|
-
await saveAccountsUnlocked(nextAccounts
|
|
2212
|
+
await saveAccountsUnlocked(nextAccounts, {
|
|
2213
|
+
expectedOnDiskAccounts: state.snapshot?.accounts,
|
|
2214
|
+
});
|
|
1652
2215
|
try {
|
|
1653
2216
|
await saveFlaggedAccountsUnlocked(nextFlagged);
|
|
1654
2217
|
state.snapshot = nextAccounts;
|
|
1655
2218
|
}
|
|
1656
2219
|
catch (error) {
|
|
1657
2220
|
try {
|
|
1658
|
-
await saveAccountsUnlocked(previousAccounts
|
|
2221
|
+
await saveAccountsUnlocked(previousAccounts, {
|
|
2222
|
+
expectedOnDiskAccounts: state.snapshot?.accounts,
|
|
2223
|
+
});
|
|
1659
2224
|
state.snapshot = previousAccounts;
|
|
1660
2225
|
}
|
|
1661
2226
|
catch (rollbackError) {
|
|
@@ -1709,7 +2274,21 @@ export async function withFlaggedStorageTransaction(handler) {
|
|
|
1709
2274
|
* @param storage - Account storage data to save
|
|
1710
2275
|
* @throws StorageError with platform-aware hints on failure
|
|
1711
2276
|
*/
|
|
1712
|
-
/**
|
|
2277
|
+
/**
|
|
2278
|
+
* Clone a loaded working snapshot without losing its optimistic-concurrency baseline.
|
|
2279
|
+
*
|
|
2280
|
+
* Baseline contract: `saveAccounts` only runs the three-way merge
|
|
2281
|
+
* (`mergeAccountSnapshot` against a fresh `loadPrimaryAccountsForMerge` read)
|
|
2282
|
+
* when the saved object is registered in `loadedAccountSnapshots` — and that
|
|
2283
|
+
* WeakMap is keyed by OBJECT IDENTITY, so registration does not survive
|
|
2284
|
+
* `structuredClone`, `JSON.parse(JSON.stringify(...))`, spread copies, or
|
|
2285
|
+
* manual reconstruction. A baseline-less save writes the object as-is
|
|
2286
|
+
* (last-writer-wins): concurrent changes are neither merged nor flagged with
|
|
2287
|
+
* ESTALE, and when the primary on disk differs from the state this process
|
|
2288
|
+
* last read or wrote the save additionally emits a one-time baseline-less
|
|
2289
|
+
* overwrite warning. Use this helper — or mutate the loaded object in place —
|
|
2290
|
+
* to keep the merge armed.
|
|
2291
|
+
*/
|
|
1713
2292
|
export function cloneTrackedAccountStorage(storage) {
|
|
1714
2293
|
const clone = structuredClone(storage);
|
|
1715
2294
|
const baseline = loadedAccountSnapshots.get(storage);
|
|
@@ -1723,8 +2302,16 @@ export async function saveAccounts(storage) {
|
|
|
1723
2302
|
withStorageLock,
|
|
1724
2303
|
saveUnlocked: async (proposed) => {
|
|
1725
2304
|
const baseline = loadedAccountSnapshots.get(proposed);
|
|
1726
|
-
const
|
|
1727
|
-
|
|
2305
|
+
const current = baseline ? await loadPrimaryAccountsForMerge() : null;
|
|
2306
|
+
if (!baseline) {
|
|
2307
|
+
// Without a baseline the three-way merge cannot run — this write is
|
|
2308
|
+
// last-writer-wins. Warn (once) if the primary changed underneath us.
|
|
2309
|
+
await warnOnBaselinelessSave(getStoragePath());
|
|
2310
|
+
}
|
|
2311
|
+
const snapshot = baseline ? mergeAccountSnapshot(baseline, current, proposed) : proposed;
|
|
2312
|
+
await saveAccountsUnlocked(snapshot, {
|
|
2313
|
+
expectedOnDiskAccounts: current?.accounts,
|
|
2314
|
+
});
|
|
1728
2315
|
if (baseline) {
|
|
1729
2316
|
for (const key of Object.keys(proposed))
|
|
1730
2317
|
Reflect.deleteProperty(proposed, key);
|