@openchambery/web 2.1.8-beta.2 → 2.1.8-beta.4
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/dist/assets/{AgentAvatar-ndlnVkjO.js → AgentAvatar-qSjSZvIG.js} +1 -1
- package/dist/assets/{AssistantView-6ta2Y6q7.js → AssistantView-Dx-mzVKK.js} +3 -3
- package/dist/assets/{DiagramView-DvZU3Yxr.js → DiagramView-CDmixuZQ.js} +1 -1
- package/dist/assets/{FadeInOnReveal-CcvpP9n1.js → FadeInOnReveal-CD5d1k_u.js} +1 -1
- package/dist/assets/{Icon-2Obqh9Fi.js → Icon-QASb6r3F.js} +1 -1
- package/dist/assets/{ImageLoadingPlaceholder-D916bbhl.js → ImageLoadingPlaceholder-BuGiEI7w.js} +1 -1
- package/dist/assets/{LoadingImage-BeOr4G0q.js → LoadingImage-FRySi1BM.js} +1 -1
- package/dist/assets/{MarkdownRenderer-Cl5V8BKz.js → MarkdownRenderer-CggK1I6D.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-losB1IeW.js → MarkdownRendererImpl-Cms-VuSl.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-PHsV91V_.js → MarkstreamRendererImpl-CrC7_3Y7.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-W-DaFFg5.js → MobileDetailNavigation-CunQ1OIb.js} +1 -1
- package/dist/assets/{MobileShareBridge-Bjzb3TK6.js → MobileShareBridge-CoRbF_fc.js} +1 -1
- package/dist/assets/MobileTabPageHeader-Bj_Lu8tt.js +2 -0
- package/dist/assets/{MultiRunWindow-BbfM6ISu.js → MultiRunWindow-BgNee8Nh.js} +1 -1
- package/dist/assets/{OnboardingScreen-DEolMFq_.js → OnboardingScreen-CHa767GT.js} +1 -1
- package/dist/assets/{OverlayScrollbar-DB7fWZUw.js → OverlayScrollbar-C7Ls30Hy.js} +1 -1
- package/dist/assets/{SettingsView-Bjo-fXIC.js → SettingsView-D0Rk6ERT.js} +10 -9
- package/dist/assets/{SettingsWindow-DdN8IePL.js → SettingsWindow-8i92GNln.js} +1 -1
- package/dist/assets/{TerminalView-BUHkhkkZ.js → TerminalView-mLwQ0UU9.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DGY_JzQg.js → ToolOutputDialog-DH6ASkUk.js} +1 -1
- package/dist/assets/{appThemeRegistry-DiuhF7sJ.js → appThemeRegistry-CHeMJdPY.js} +1 -1
- package/dist/assets/{button-Bdq9SUvZ.js → button-DOrwfkre.js} +1 -1
- package/dist/assets/{collapsible-C7PP5l16.js → collapsible-BJwPZMpo.js} +1 -1
- package/dist/assets/{diagram-BBlwWp3m.js → diagram-BFevUcKu.js} +1 -1
- package/dist/assets/{dialog-DHmUjm6M.js → dialog-FSXwOR2i.js} +1 -1
- package/dist/assets/{dropdown-menu.styles-XSr3iA4S.js → dropdown-menu.styles-CfTQwJHe.js} +1 -1
- package/dist/assets/{es-BxLn_G_Q.js → es-VbQQdCdi.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-BHA_5cBo.js → fileMentionTouchSelection-CSelAE3Z.js} +1 -1
- package/dist/assets/{fr-LROiZho7.js → fr-BotRbS9t.js} +1 -1
- package/dist/assets/{gitApi-BdyssA-O.js → gitApi-B123oHQk.js} +1 -1
- package/dist/assets/{input-De00oOal.js → input-Ctgb06ot.js} +1 -1
- package/dist/assets/{insertionBoundaries-DIohQnVE.js → insertionBoundaries-cfka-ff_.js} +1 -1
- package/dist/assets/{ja-DiErdLtA.js → ja-l_gq6Coh.js} +1 -1
- package/dist/assets/{ko-srYeEs4u.js → ko-yhU5rfe5.js} +1 -1
- package/dist/assets/{main-BETi-fsf.js → main-DJhLxkOQ.js} +10 -10
- package/dist/assets/{main-DUrFgqZL.js → main-IO37JIWf.js} +2 -2
- package/dist/assets/{markdownHeightCache-D44E3PrO.js → markdownHeightCache-B6drDnVO.js} +1 -1
- package/dist/assets/{mcpQueries-qCvadKyt.js → mcpQueries-Digedyij.js} +1 -1
- package/dist/assets/miniChat-mE8pRELI.js +2 -0
- package/dist/assets/mobile-Cia2889g.js +2 -0
- package/dist/assets/{multirun-C2aiem4S.js → multirun-C1I9Attc.js} +1 -1
- package/dist/assets/{pl-CzKfWMm9.js → pl-BkynZgOJ.js} +1 -1
- package/dist/assets/{projectMeta-DV6HExaM.js → projectMeta-C2WeOasQ.js} +1 -1
- package/dist/assets/{pt-BR-CUmrnVae.js → pt-BR-Dw_Dokez.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-FpoAJUKI.js +2 -0
- package/dist/assets/{renderMobileApp-Ds-6h0OM.js → renderMobileApp-DrT0ZwHw.js} +6 -6
- package/dist/assets/{runtimeConfig-CBy91BoB.js → runtimeConfig-yW48patn.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-C7wwKdBg.js → runtimeEndpointReset-CMmN7tH1.js} +5 -5
- package/dist/assets/{scheduledTaskNavigation-D7yhTPgz.js → scheduledTaskNavigation-DEMPeoot.js} +1 -1
- package/dist/assets/sessionChangeSummary-D1YrkHbB.js +1 -0
- package/dist/assets/{sessionLookup-D0uMihAO.js → sessionLookup-DrKLmlMv.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-aIcw2DJh.js → sortable-tabs-strip-BVnwfMAa.js} +1 -1
- package/dist/assets/speechSettingsQueries-BNPsqHp5.js +4 -0
- package/dist/assets/{store-BYLl4QL2.js → store-COtDAfPx.js} +2 -2
- package/dist/assets/{textarea-DNadM0DL.js → textarea-DJ52TDJj.js} +1 -1
- package/dist/assets/{tooltip-nHGca5md.js → tooltip-JgW0lJaV.js} +1 -1
- package/dist/assets/{uk-DGZ3Hl33.js → uk-DSl09YqU.js} +1 -1
- package/dist/assets/{useAppFontEffects-ByC3DE9D.js → useAppFontEffects-BUJapti0.js} +51 -53
- package/dist/assets/{useDesktopWindowControlsLayout-CbrmthFZ.js → useDesktopWindowControlsLayout-Dlh9xNoE.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-BEpQGzay.js → useEffectiveDirectory-2dkaAaxr.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-Cm0CuwA9.js → useMobileNavigationStore-BRQBoMI1.js} +6 -6
- package/dist/assets/{useWorkerHighlightedLines-D5mEoRi2.js → useWorkerHighlightedLines-BRAfFOVn.js} +1 -1
- package/dist/assets/vendor-codemirror-autocomplete-7c2L_4dM.js +12 -0
- package/dist/assets/vendor-codemirror-commands-B8KrBQ9B.js +1 -0
- package/dist/assets/vendor-codemirror-lang-angular-DXOgWEVj.js +23 -0
- package/dist/assets/{vendor-codemirror-lang-cpp-C3UKgJaQ.js → vendor-codemirror-lang-cpp-Dh8C3xqP.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-go-Cv5NYdGm.js → vendor-codemirror-lang-go-BXAJ1_dh.js} +5 -5
- package/dist/assets/{vendor-codemirror-lang-java-D6hBTIgD.js → vendor-codemirror-lang-java-DAyCHRhk.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-jinja-ehGXhAov.js → vendor-codemirror-lang-jinja-0TICrMfO.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-json-tHviF6YN.js → vendor-codemirror-lang-json-mLulNbwB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-less-B-yDfSDo.js → vendor-codemirror-lang-less-CNvptX3P.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-liquid-BdoLH4KN.js → vendor-codemirror-lang-liquid-8g5seytq.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-markdown-CV1IO6Qs.js +6 -0
- package/dist/assets/{vendor-codemirror-lang-php-Ch8GW4Xz.js → vendor-codemirror-lang-php-xP7rf28_.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-python-YM2WVLHF.js +9 -0
- package/dist/assets/{vendor-codemirror-lang-rust-BEUlElU6.js → vendor-codemirror-lang-rust-CtafR0gt.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-sass-B4iav_Ps.js +1 -0
- package/dist/assets/{vendor-codemirror-lang-sql-BnZql454.js → vendor-codemirror-lang-sql-C6q09UUZ.js} +2 -2
- package/dist/assets/{vendor-codemirror-lang-vue-BCwQBvLR.js → vendor-codemirror-lang-vue-DIB78UsR.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-wast-mrtA2eML.js → vendor-codemirror-lang-wast-C_JVdOCF.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-xml-ClAh-G2D.js → vendor-codemirror-lang-xml-DIuzY_d4.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-yaml-CjL8XEf2.js → vendor-codemirror-lang-yaml-BO0hIfQM.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DGDYune4.js → vendor-codemirror-language-data-Ctd4WxgW.js} +2 -2
- package/dist/assets/vendor-codemirror-search-ZqtrPLjU.js +2 -0
- package/dist/assets/{vendor-replit-codemirror-vim-CEibx3EP.js → vendor-replit-codemirror-vim-DDj0dSVY.js} +6 -6
- package/dist/assets/{zh-CN-UwxgbjmI.js → zh-CN-NtO1bA00.js} +1 -1
- package/dist/assets/{zh-TW-Bc9XoqY8.js → zh-TW-CF0z_L0k.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/mini-chat.html +2 -2
- package/dist/mobile.html +2 -2
- package/package.json +1 -1
- package/server/lib/config-sync/DOCUMENTATION.md +10 -5
- package/server/lib/config-sync/engine.js +3 -2
- package/server/lib/config-sync/magpie-auth.js +111 -0
- package/server/lib/config-sync/magpie-auth.test.js +79 -0
- package/server/lib/config-sync/magpie.js +81 -9
- package/server/lib/config-sync/magpie.test.js +120 -5
- package/server/lib/config-sync/receiver.js +3 -1
- package/server/lib/config-sync/routes.test.js +3 -2
- package/server/lib/opencode/DOCUMENTATION.md +1 -0
- package/server/lib/opencode/lifecycle.js +4 -0
- package/server/lib/opencode/lifecycle.test.js +7 -2
- package/dist/assets/MobileTabPageHeader-qbGYMq9u.js +0 -2
- package/dist/assets/miniChat-jC1_fkBZ.js +0 -2
- package/dist/assets/mobile-D-5-hKu6.js +0 -2
- package/dist/assets/renderElectronMiniChatApp-CI-5mVAG.js +0 -2
- package/dist/assets/sessionChangeSummary-BSFJiKHQ.js +0 -2
- package/dist/assets/speechSettingsQueries-Bd3icbFp.js +0 -4
- package/dist/assets/vendor-codemirror-autocomplete-DHomsvW7.js +0 -12
- package/dist/assets/vendor-codemirror-commands-C9DCaJ0C.js +0 -1
- package/dist/assets/vendor-codemirror-lang-angular-BDXnzWgq.js +0 -23
- package/dist/assets/vendor-codemirror-lang-markdown-Bt-vLz9k.js +0 -6
- package/dist/assets/vendor-codemirror-lang-python-B4naLXMo.js +0 -9
- package/dist/assets/vendor-codemirror-lang-sass-ibtp2iUB.js +0 -1
- package/dist/assets/vendor-codemirror-search-D2SOgzph.js +0 -2
package/dist/index.html
CHANGED
|
@@ -552,7 +552,7 @@
|
|
|
552
552
|
opacity: 0.72;
|
|
553
553
|
}
|
|
554
554
|
</style>
|
|
555
|
-
<script type="module" crossorigin src="/assets/main-
|
|
555
|
+
<script type="module" crossorigin src="/assets/main-IO37JIWf.js"></script>
|
|
556
556
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
557
557
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-C8w0nlKy.js">
|
|
558
558
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-3ad3yzM7.js">
|
|
@@ -572,7 +572,7 @@
|
|
|
572
572
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-bN5IYQau.js">
|
|
573
573
|
<link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
|
|
574
574
|
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
|
|
575
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
575
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-yW48patn.js">
|
|
576
576
|
<link rel="stylesheet" crossorigin href="/assets/src-C9BNQK0O.css">
|
|
577
577
|
</head>
|
|
578
578
|
<body class="h-full bg-background text-foreground">
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-mE8pRELI.js"></script>
|
|
78
78
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
79
79
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-C8w0nlKy.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-3ad3yzM7.js">
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-bN5IYQau.js">
|
|
95
95
|
<link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
|
|
96
96
|
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
|
|
97
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
97
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-yW48patn.js">
|
|
98
98
|
<link rel="stylesheet" crossorigin href="/assets/src-C9BNQK0O.css">
|
|
99
99
|
</head>
|
|
100
100
|
<body class="h-full bg-background text-foreground">
|
package/dist/mobile.html
CHANGED
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
})();
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-Cia2889g.js"></script>
|
|
67
67
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
68
68
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-C8w0nlKy.js">
|
|
69
69
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-3ad3yzM7.js">
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-bN5IYQau.js">
|
|
84
84
|
<link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
|
|
85
85
|
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
|
|
86
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
86
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-yW48patn.js">
|
|
87
87
|
<link rel="stylesheet" crossorigin href="/assets/src-C9BNQK0O.css">
|
|
88
88
|
</head>
|
|
89
89
|
<body class="h-full bg-background text-foreground">
|
package/package.json
CHANGED
|
@@ -19,6 +19,7 @@ Own the direction-agnostic OpenCode config sync contract: allowlist planning on
|
|
|
19
19
|
| `engine.js` | `applyConfigSyncPlan` orchestration for **push** (prepare → putTar* → finalize) |
|
|
20
20
|
| `credentials.js` | V2 credential API transfer, validation, account merge and in-memory rollback |
|
|
21
21
|
| `magpie.js` | Host-local Magpie discovery, verified CLI provisioning, native backup/restore |
|
|
22
|
+
| `magpie-auth.js` | Bounded portable authorization export, plugin-store merge and account verification |
|
|
22
23
|
| `target-id.js` | `ssh:<instanceId>` namespace helper |
|
|
23
24
|
|
|
24
25
|
## Contract shape
|
|
@@ -67,7 +68,7 @@ Routes under `/api/openchamber/config-sync/*` (global `/api` auth gate: session
|
|
|
67
68
|
| GET | `/credentials/inventory` | Current-service credential byte count, without secrets |
|
|
68
69
|
| PUT | `/credentials` | Bounded credential import for local pull and SSH-forwarded transfer |
|
|
69
70
|
| GET | `/magpie/inventory` | `{ magpie: { available, installed } }`, metadata only; never installs |
|
|
70
|
-
| PUT | `/magpie` |
|
|
71
|
+
| PUT | `/magpie` | Magpie config/authorization restore and verification for local pull and SSH-forwarded transfer; installs missing CLI |
|
|
71
72
|
| POST | `/finalize` | Confirm backup generation + success receipt |
|
|
72
73
|
| POST | `/abort` | Clear inflight |
|
|
73
74
|
|
|
@@ -84,12 +85,16 @@ Routes under `/api/openchamber/config-sync/*` (global `/api` auth gate: session
|
|
|
84
85
|
|
|
85
86
|
The existing sync wizard discovers Magpie on the **source** for every preview. A push uses the local host's inventory; a pull uses the selected remote host's inventory (HTTP through the SSH forward, direct, or E2EE Relay). `magpieAvailable` survives selection filtering. The optional `magpie` plan entry is `{ bytes: 0 }` when selected: backup size is unknown until export, not counted as a zero-byte archive. Switching direction clears the Magpie selection and discovers the new source. A selected source disappearing is a failure, never an empty successful sync. This is discovery during the existing explicit sync flow, not a background file watcher.
|
|
86
87
|
|
|
87
|
-
Discovery checks `MAGPIE_BINARY`, PATH, the user's `.local/bin`, macOS application bundles and Homebrew paths, the standard Windows user installation path, then OpenChamber's managed executable. Symlinks resolve before locating portable data. Installed config honors absolute `XDG_CONFIG_HOME`, otherwise `~/.config/magpie`; portable config follows `.portable` / adjacent `data`, including the macOS app parent and an applicable AppImage directory. A shared Applications/home/download directory's `data` is not adopted over installed config.
|
|
88
|
+
Discovery checks `MAGPIE_BINARY`, PATH, the user's `.local/bin`, macOS application bundles and Homebrew paths, the standard Windows user installation path, then OpenChamber's managed executable. Symlinks resolve before locating portable data. Installed config honors absolute `XDG_CONFIG_HOME`, otherwise `~/.config/magpie`; portable config follows `.portable` / adjacent `data`, including the macOS app parent and an applicable AppImage directory. A shared Applications/home/download directory's `data` is not adopted over installed config. The source is available only when a verified CLI is installed and providers, settings, profiles, library, plugin config or authorization stores exist. Leftover config without an installed CLI does not expose the sync option. Magpie's checkbox and explanatory copy form a separate conditional group, default unchecked; general OpenCode sync copy remains independent. Metadata does not expose paths or secrets. Read errors fail discovery.
|
|
88
89
|
|
|
89
|
-
Selecting Magpie uses its native `backup` / `restore` with configuration, API keys, profiles, agent models, library and portable plugin declarations.
|
|
90
|
+
Selecting Magpie uses its native `backup` / `restore` with configuration, API keys, profiles, agent models, library and portable plugin declarations. Native provider/group restore merges by ID and does not propagate deletions; settings and library follow Magpie's own restore semantics, retaining machine-local settings. This is a full native bundle, not a provider-only restore.
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
The v2 envelope additionally carries recognized plugin API/OAuth accounts and self-contained saved Codex credentials. The source CLI account inventory limits export to recognized providers; hidden and orphaned plugin sign-ins are excluded. Plugin accounts merge by their native account key into `plugin-auth.json` under Magpie's exclusive-create lock, using a private atomic replacement. Destination-only accounts remain; repeated transfers do not append duplicates. Codex uses native `accounts import codex --yes`, which owns account identity/deduplication and the OS-level `logins.json` lock. OpenChamber never writes `logins.json` or copies machine-local account homes. Other subscription formats and accounts discovered only from external clients are not inferred or silently treated as transferred: unsupported active source accounts produce `sign_in_required`. An import re-reads `accounts --json` and requires all transferred plugin account counts and Codex identities to be recognized before returning success. This checks discovery, not successful model inference or future token renewal; revoked credentials still require login.
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
Missing destination executables are installed only when Magpie is explicitly selected for import, never in preview or ordinary OpenCode sync. The host selects the official pure CLI asset for macOS/Linux/Windows x64/arm64 from `https://usemagpie.ai/api/latest`, verifies SHA-256, checks `--version`, then atomically publishes it under `<OPENCHAMBER_DATA_DIR>/tools/magpie/` (default `~/.config/openchamber`). Existing desktop clients and CLIs are reused. Installation is process-local single-flight, needs no global PATH change or desktop environment, and fails explicitly for unsupported platforms or unverifiable downloads.
|
|
95
|
+
|
|
96
|
+
Selected sync also publishes `~/.local/bin/magpie` (`magpie.exe` on Windows), a path searched explicitly by `opencode-magpie-plugin`. POSIX uses a symlink, Windows a hard link for installed-mode data and a symlink for portable data; unsupported linking fails explicitly. Existing valid user commands using the same data directory are retained; conflicting/broken entries are not overwritten. Shared-service and private OpenCode startup repair the command entry for an already managed CLI before launching OpenCode; they never download Magpie just because OpenChamber starts. SSH/direct/Relay push and pull apply Magpie before OpenCode config archives, preventing a newly synced plugin from loading before its dependency is installed. Native agent settings skipped before sign-in are retried after authorization succeeds.
|
|
97
|
+
|
|
98
|
+
The `magpie` transfer kind exports a bounded `magpie-sync-v2` JSON envelope containing the encrypted native backup, a random per-transfer passphrase and portable authorizations. The envelope is sensitive and stays in memory on authenticated sync transports; Relay adds its existing E2EE. New receivers accept old `magpie-sync-v1` config-only envelopes with the original warning behavior; old receivers reject v2 rather than silently discarding its accounts. Upgrade both hosts for authorization sync. Validate the entire envelope before installation or restore. Passphrases go to child stdin, never argv, run logs or disk. Private temporary directories hold native encrypted backups and, during native Codex import only, a mode-0600 credential file; both are removed on success/failure. Raw child output is sanitized before reporting. A zero exit with genuinely missing/unsupported sign-ins or unavailable agent settings returns `magpieWarnings` (`sign_in_required`, `agent_settings_skipped`). The sign-in warning is recomputed after authorization transfer, not copied from the pre-transfer native restore. Receipts carry these codes through SSH/direct/Relay and desktop IPC to localized completion warnings. Provider/API-key, plugin-install, library, account import and verification failures remain errors. CLI failure can leave configuration and earlier accounts applied; retry merges idempotently. There is no cross-application rollback, and process termination can interrupt cleanup.
|
|
94
99
|
|
|
95
100
|
`/put/magpie` requires a selected prepared plan and finalize requires its receipt. Standalone `/magpie` is the corresponding authenticated host operation for SSH and local pull. Both use one host-local import/export lock. Desktop main only moves bytes and resolves the local host at call time; the web host owns process execution and installation, including under backend OTA. Web/mobile hosts can receive this operation; the initiating wizard remains desktop-only. No credential values enter previews or sync-run summaries.
|
|
@@ -65,6 +65,7 @@ export const applyConfigSyncPlan = async ({
|
|
|
65
65
|
|
|
66
66
|
await executor.prepare(plan, { syncRunId });
|
|
67
67
|
|
|
68
|
+
if (magpiePayload) await executor.putTar({ kind: 'magpie', payload: magpiePayload });
|
|
68
69
|
if (configTarBuffer) {
|
|
69
70
|
await executor.putTar({ kind: 'config', payload: configTarBuffer });
|
|
70
71
|
}
|
|
@@ -75,9 +76,8 @@ export const applyConfigSyncPlan = async ({
|
|
|
75
76
|
// Versioned credential payload; never a database or filesystem archive.
|
|
76
77
|
await executor.putTar({ kind: 'credentials', payload: credentialPayload });
|
|
77
78
|
}
|
|
78
|
-
if (magpiePayload) await executor.putTar({ kind: 'magpie', payload: magpiePayload });
|
|
79
79
|
|
|
80
|
-
await executor.finalize(plan, { syncRunId });
|
|
80
|
+
const receipt = await executor.finalize(plan, { syncRunId });
|
|
81
81
|
|
|
82
82
|
return {
|
|
83
83
|
ok: true,
|
|
@@ -88,6 +88,7 @@ export const applyConfigSyncPlan = async ({
|
|
|
88
88
|
agentsRoot: plan.agentsRoot ? { fileCount: plan.agentsRoot.fileCount } : null,
|
|
89
89
|
providerCredentials: plan.providerCredentials ? { bytes: plan.providerCredentials.bytes } : null,
|
|
90
90
|
plan,
|
|
91
|
+
...(receipt?.magpieWarnings?.length ? { magpieWarnings: receipt.magpieWarnings } : {}),
|
|
91
92
|
};
|
|
92
93
|
};
|
|
93
94
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
|
|
6
|
+
const MAX_AUTH_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
const accountKey = (value) => typeof value === 'string' && /^[\w.-]+(?:#[\w.-]+)?$/.test(value)
|
|
9
|
+
&& !['__proto__', 'constructor', 'prototype'].includes(value);
|
|
10
|
+
const portableLogin = (row, plugins) => object(row) && typeof row.user === 'string'
|
|
11
|
+
&& ((row.agent === 'codex' && !row.home && object(row.auth) && object(row.auth.tokens))
|
|
12
|
+
|| (typeof row.agent === 'string' && row.agent.startsWith('plugin:')
|
|
13
|
+
&& accountKey(row.home) && Object.hasOwn(plugins, row.home)
|
|
14
|
+
&& row.agent === `plugin:${row.home.split('#')[0]}`));
|
|
15
|
+
|
|
16
|
+
const readStore = async (file, fallback) => {
|
|
17
|
+
try {
|
|
18
|
+
if ((await fs.stat(file)).size > MAX_AUTH_BYTES) throw new Error();
|
|
19
|
+
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code === 'ENOENT') return fallback;
|
|
22
|
+
throw new Error('Unable to read Magpie authorization store');
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const validateMagpieAuth = (value) => {
|
|
27
|
+
if (!object(value) || !object(value.plugins) || !Array.isArray(value.logins)
|
|
28
|
+
|| typeof value.incomplete !== 'boolean' || JSON.stringify(value).length > MAX_AUTH_BYTES
|
|
29
|
+
|| Object.entries(value.plugins).some(([key, auth]) => !accountKey(key) || !object(auth)
|
|
30
|
+
|| !['api', 'oauth'].includes(auth.type)
|
|
31
|
+
|| (auth.type === 'api' ? typeof auth.key !== 'string' || !auth.key
|
|
32
|
+
: typeof auth.access !== 'string' || typeof auth.refresh !== 'string'))
|
|
33
|
+
|| value.logins.some((row) => !portableLogin(row, value.plugins))) {
|
|
34
|
+
throw new Error('Invalid Magpie authorization payload');
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const collectMagpieAuth = async (dir, accounts) => {
|
|
40
|
+
if (!Array.isArray(accounts) || accounts.some((row) => !object(row) || typeof row.agent !== 'string')) {
|
|
41
|
+
throw new Error('Unable to read source Magpie accounts');
|
|
42
|
+
}
|
|
43
|
+
const plugins = await readStore(path.join(dir, 'plugin-auth.json'), {});
|
|
44
|
+
const rows = await readStore(path.join(dir, 'logins.json'), []);
|
|
45
|
+
if (!object(plugins) || !Array.isArray(rows) || rows.some((row) => !object(row))) {
|
|
46
|
+
throw new Error('Invalid Magpie authorization store');
|
|
47
|
+
}
|
|
48
|
+
const available = new Set(accounts.map((row) => row.agent));
|
|
49
|
+
const visiblePlugins = Object.fromEntries(Object.entries(plugins).filter(([key]) => available.has(key.split('#')[0])
|
|
50
|
+
&& !rows.some((row) => row.agent === `plugin:${key.split('#')[0]}` && row.home === key && row.hidden)));
|
|
51
|
+
const logins = rows.filter((row) => !row.hidden && portableLogin(row, visiblePlugins));
|
|
52
|
+
return validateMagpieAuth({ plugins: visiblePlugins, logins,
|
|
53
|
+
incomplete: accounts.some((account) => !Object.keys(visiblePlugins).some((key) => key.split('#')[0] === account.agent)
|
|
54
|
+
&& !logins.some((row) => row.agent === account.agent && row.user === account.user)) });
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const writeStore = async (file, value) => {
|
|
58
|
+
const staging = `${file}.${randomBytes(8).toString('hex')}.tmp`;
|
|
59
|
+
try {
|
|
60
|
+
await fs.writeFile(staging, JSON.stringify(value), { mode: 0o600, flag: 'wx' });
|
|
61
|
+
await fs.rename(staging, file);
|
|
62
|
+
} finally { await fs.rm(staging, { force: true }); }
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Use Magpie's own lock filenames; never steal a lock from a running token refresh.
|
|
66
|
+
const lockStore = async (file) => {
|
|
67
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
68
|
+
try {
|
|
69
|
+
const handle = await fs.open(`${file}.lock`, 'wx', 0o600);
|
|
70
|
+
await handle.close();
|
|
71
|
+
return () => fs.rm(`${file}.lock`, { force: true });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code !== 'EEXIST') throw new Error('Unable to lock Magpie authorization store');
|
|
74
|
+
await delay(50);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
throw new Error('Magpie authorization store is busy; retry sync');
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const restoreMagpieAuth = async (dir, input, importCodex) => {
|
|
81
|
+
const auth = validateMagpieAuth(input);
|
|
82
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
83
|
+
const pluginFile = path.join(dir, 'plugin-auth.json');
|
|
84
|
+
const unlockPlugins = await lockStore(pluginFile);
|
|
85
|
+
try {
|
|
86
|
+
const plugins = await readStore(pluginFile, {});
|
|
87
|
+
if (!object(plugins)) throw new Error('Invalid destination Magpie authorization store');
|
|
88
|
+
await writeStore(pluginFile, { ...plugins, ...auth.plugins });
|
|
89
|
+
} finally { await unlockPlugins(); }
|
|
90
|
+
// Codex's native importer owns its OS-level logins lock, account deduplication and identity.
|
|
91
|
+
for (const row of auth.logins) {
|
|
92
|
+
if (row.agent === 'codex' && !row.hidden) await importCodex(row.auth);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const verifyMagpieAuth = (auth, accounts) => {
|
|
97
|
+
if (!Array.isArray(accounts) || accounts.some((row) => !object(row) || typeof row.agent !== 'string')) {
|
|
98
|
+
throw new Error('Unable to verify Magpie accounts');
|
|
99
|
+
}
|
|
100
|
+
const expected = new Map();
|
|
101
|
+
for (const key of Object.keys(auth.plugins)) {
|
|
102
|
+
const id = key.split('#')[0];
|
|
103
|
+
expected.set(id, (expected.get(id) || 0) + 1);
|
|
104
|
+
}
|
|
105
|
+
const missingAccount = auth.logins.some((row) => !row.hidden
|
|
106
|
+
&& !accounts.some((account) => account.agent === row.agent.replace(/^plugin:/, '')
|
|
107
|
+
&& String(account.user).toLowerCase() === row.user.toLowerCase()));
|
|
108
|
+
if (missingAccount || [...expected].some(([id, count]) => accounts.filter((row) => row.agent === id).length < count)) {
|
|
109
|
+
throw new Error('Magpie authorization saved but some accounts are not recognized; check destination plugins and retry');
|
|
110
|
+
}
|
|
111
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { collectMagpieAuth, restoreMagpieAuth, validateMagpieAuth, verifyMagpieAuth } from './magpie-auth.js';
|
|
6
|
+
|
|
7
|
+
const dirs = [];
|
|
8
|
+
const directory = async () => {
|
|
9
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'magpie-auth-test-'));
|
|
10
|
+
dirs.push(dir);
|
|
11
|
+
return dir;
|
|
12
|
+
};
|
|
13
|
+
const auth = { plugins: { 'cursor#one': { type: 'oauth', access: 'fixture-access', refresh: 'fixture-refresh', expires: 123 } }, logins: [], incomplete: false };
|
|
14
|
+
afterEach(async () => { vi.restoreAllMocks(); await Promise.all(dirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); });
|
|
15
|
+
|
|
16
|
+
describe('Magpie authorization transfer', () => {
|
|
17
|
+
it('exports recognized plugin accounts, excludes hidden/stale accounts and flags unsupported subscriptions', async () => {
|
|
18
|
+
const dir = await directory();
|
|
19
|
+
await fs.writeFile(path.join(dir, 'plugin-auth.json'), JSON.stringify({ ...auth.plugins,
|
|
20
|
+
stale: { type: 'api', key: 'fixture' }, 'cursor#hidden': { type: 'api', key: 'fixture' } }));
|
|
21
|
+
await fs.writeFile(path.join(dir, 'logins.json'), JSON.stringify([
|
|
22
|
+
{ agent: 'plugin:cursor', user: 'hidden', home: 'cursor#hidden', hidden: 'mark' },
|
|
23
|
+
{ agent: 'chatgpt-api', user: 'fixture', auth: { clientId: 'machine-specific' } },
|
|
24
|
+
]));
|
|
25
|
+
const result = await collectMagpieAuth(dir, [{ agent: 'cursor' }, { agent: 'chatgpt-api', user: 'fixture' }]);
|
|
26
|
+
expect(result.plugins).toEqual(auth.plugins);
|
|
27
|
+
expect(result.logins).toEqual([]);
|
|
28
|
+
expect(result.incomplete).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('does not turn corrupted stores into successful empty credentials', async () => {
|
|
32
|
+
const dir = await directory();
|
|
33
|
+
await fs.writeFile(path.join(dir, 'plugin-auth.json'), '{broken');
|
|
34
|
+
await expect(collectMagpieAuth(dir, [])).rejects.toThrow('authorization store');
|
|
35
|
+
await expect(restoreMagpieAuth(dir, auth, vi.fn())).rejects.toThrow('authorization store');
|
|
36
|
+
expect(await fs.readFile(path.join(dir, 'plugin-auth.json'), 'utf8')).toBe('{broken');
|
|
37
|
+
expect(await fs.readdir(dir)).toEqual(['plugin-auth.json']);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('merges repeated imports without deleting unrelated accounts and releases the plugin lock before native imports', async () => {
|
|
41
|
+
const dir = await directory();
|
|
42
|
+
await fs.writeFile(path.join(dir, 'plugin-auth.json'), '{"other":{"type":"api","key":"keep"}}');
|
|
43
|
+
const input = { ...auth, logins: [{ agent: 'codex', user: 'fixture', auth: { tokens: {} } }] };
|
|
44
|
+
const importCodex = vi.fn(async () => {
|
|
45
|
+
await expect(fs.stat(path.join(dir, 'plugin-auth.json.lock'))).rejects.toThrow();
|
|
46
|
+
});
|
|
47
|
+
await restoreMagpieAuth(dir, input, importCodex);
|
|
48
|
+
await restoreMagpieAuth(dir, input, importCodex);
|
|
49
|
+
expect(JSON.parse(await fs.readFile(path.join(dir, 'plugin-auth.json'), 'utf8'))).toEqual({ other: { type: 'api', key: 'keep' }, ...auth.plugins });
|
|
50
|
+
expect((await fs.stat(path.join(dir, 'plugin-auth.json'))).mode & 0o777).toBe(0o600);
|
|
51
|
+
expect(await fs.readdir(dir)).toEqual(['plugin-auth.json']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('keeps successful plugin writes when a native import fails, so retry can finish', async () => {
|
|
55
|
+
const dir = await directory();
|
|
56
|
+
await expect(restoreMagpieAuth(dir, { ...auth, logins: [{ agent: 'codex', user: 'fixture', auth: { tokens: {} } }] },
|
|
57
|
+
async () => { throw new Error('fixture failure'); })).rejects.toThrow();
|
|
58
|
+
expect(JSON.parse(await fs.readFile(path.join(dir, 'plugin-auth.json'), 'utf8'))).toEqual(auth.plugins);
|
|
59
|
+
expect(await fs.readdir(dir)).toEqual(['plugin-auth.json']);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('preserves the previous store and cleans staging files when atomic publication fails', async () => {
|
|
63
|
+
const dir = await directory();
|
|
64
|
+
const file = path.join(dir, 'plugin-auth.json');
|
|
65
|
+
await fs.writeFile(file, '{"other":{"type":"api","key":"keep"}}');
|
|
66
|
+
vi.spyOn(fs, 'rename').mockRejectedValueOnce(new Error('fixture rename failure'));
|
|
67
|
+
await expect(restoreMagpieAuth(dir, auth, vi.fn())).rejects.toThrow();
|
|
68
|
+
expect(JSON.parse(await fs.readFile(file, 'utf8'))).toEqual({ other: { type: 'api', key: 'keep' } });
|
|
69
|
+
expect(await fs.readdir(dir)).toEqual(['plugin-auth.json']);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('rejects unsafe shapes and verifies every expected account rather than one account per provider', () => {
|
|
73
|
+
expect(() => validateMagpieAuth({ ...auth, plugins: { '../path': { type: 'api', key: 'fixture' } } })).toThrow();
|
|
74
|
+
const two = { ...auth, plugins: { ...auth.plugins, 'cursor#two': auth.plugins['cursor#one'] } };
|
|
75
|
+
expect(() => verifyMagpieAuth(two, [{ agent: 'cursor' }])).toThrow('not recognized');
|
|
76
|
+
expect(() => verifyMagpieAuth(two, [{ agent: 'cursor' }, { agent: 'cursor' }])).not.toThrow();
|
|
77
|
+
expect(() => verifyMagpieAuth(auth, {})).toThrow('verify');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -3,9 +3,10 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { createHash, randomBytes } from 'node:crypto';
|
|
6
|
+
import { collectMagpieAuth, restoreMagpieAuth, validateMagpieAuth, verifyMagpieAuth } from './magpie-auth.js';
|
|
6
7
|
|
|
7
8
|
const MAX_BYTES = 64 * 1024 * 1024;
|
|
8
|
-
const CONFIG_FILES = ['providers.json', 'settings.json', 'profiles.json', 'library.json', 'plugins.json'];
|
|
9
|
+
const CONFIG_FILES = ['providers.json', 'settings.json', 'profiles.json', 'library.json', 'plugins.json', 'plugin-auth.json', 'logins.json'];
|
|
9
10
|
const exists = async (file) => {
|
|
10
11
|
try { return await fs.stat(file); }
|
|
11
12
|
catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null; throw error; }
|
|
@@ -103,7 +104,7 @@ export function createMagpieSync({
|
|
|
103
104
|
|
|
104
105
|
const inventory = async () => {
|
|
105
106
|
const binary = await locate();
|
|
106
|
-
return { available: await hasConfig(await dataDir(binary)), installed: Boolean(binary) };
|
|
107
|
+
return { available: Boolean(binary) && await hasConfig(await dataDir(binary)), installed: Boolean(binary) };
|
|
107
108
|
};
|
|
108
109
|
|
|
109
110
|
const downloadBytes = async (url, limit) => {
|
|
@@ -119,9 +120,38 @@ export function createMagpieSync({
|
|
|
119
120
|
return Buffer.concat(chunks);
|
|
120
121
|
};
|
|
121
122
|
|
|
123
|
+
const publishExecutable = async (binary) => {
|
|
124
|
+
const entry = path.join(home(), '.local', 'bin', platform === 'win32' ? 'magpie.exe' : 'magpie');
|
|
125
|
+
if (await exists(entry)) {
|
|
126
|
+
if (await fs.realpath(entry) === binary) return binary;
|
|
127
|
+
const version = await run(entry, ['--version'], { env, timeout: 10_000 });
|
|
128
|
+
if (!/^magpie\s+\S+/m.test(version)) throw new Error('Magpie command conflicts with an existing executable');
|
|
129
|
+
if (await dataDir(await fs.realpath(entry)) !== await dataDir(binary)) {
|
|
130
|
+
throw new Error('Magpie command uses a different portable configuration; resolve the command conflict before syncing');
|
|
131
|
+
}
|
|
132
|
+
return binary;
|
|
133
|
+
}
|
|
134
|
+
await fs.mkdir(path.dirname(entry), { recursive: true });
|
|
135
|
+
try {
|
|
136
|
+
if (platform === 'win32' && await dataDir(binary) === configDir()) await fs.link(binary, entry);
|
|
137
|
+
else await fs.symlink(binary, entry, 'file');
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error.code !== 'EEXIST' || !await exists(entry)) throw new Error('Unable to publish Magpie command for plugins');
|
|
140
|
+
}
|
|
141
|
+
return binary;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const repairInstallation = async () => {
|
|
145
|
+
const binary = managedBinary();
|
|
146
|
+
if (!(await exists(binary))?.isFile()) return;
|
|
147
|
+
const version = await run(binary, ['--version'], { env, timeout: 10_000 });
|
|
148
|
+
if (!/^magpie\s+\S+/m.test(version)) throw new Error('Invalid managed Magpie executable');
|
|
149
|
+
await publishExecutable(await fs.realpath(binary));
|
|
150
|
+
};
|
|
151
|
+
|
|
122
152
|
const ensure = async () => {
|
|
123
153
|
const binary = await locate();
|
|
124
|
-
if (binary) return binary;
|
|
154
|
+
if (binary) return publishExecutable(binary);
|
|
125
155
|
if (installing) return installing;
|
|
126
156
|
installing = (async () => {
|
|
127
157
|
const cpu = { x64: 'amd64', arm64: 'arm64' }[arch];
|
|
@@ -146,7 +176,7 @@ export function createMagpieSync({
|
|
|
146
176
|
if (!/^magpie\s+\S+/m.test(version)) throw new Error('Invalid downloaded Magpie CLI');
|
|
147
177
|
await fs.rename(staging, target);
|
|
148
178
|
} finally { await fs.rm(staging, { force: true }); }
|
|
149
|
-
return target;
|
|
179
|
+
return publishExecutable(target);
|
|
150
180
|
})();
|
|
151
181
|
try { return await installing; }
|
|
152
182
|
finally { installing = null; }
|
|
@@ -165,6 +195,7 @@ export function createMagpieSync({
|
|
|
165
195
|
|
|
166
196
|
return {
|
|
167
197
|
inventory,
|
|
198
|
+
repairInstallation,
|
|
168
199
|
download: () => exclusive(async () => {
|
|
169
200
|
if (!(await inventory()).available) throw new Error('No Magpie configuration found on source');
|
|
170
201
|
const binary = await ensure();
|
|
@@ -174,7 +205,14 @@ export function createMagpieSync({
|
|
|
174
205
|
const stat = await fs.stat(file);
|
|
175
206
|
if (stat.size > MAX_BYTES) throw new Error('Magpie backup exceeds size limit');
|
|
176
207
|
const backup = await fs.readFile(file);
|
|
177
|
-
|
|
208
|
+
let authorization;
|
|
209
|
+
try {
|
|
210
|
+
const accounts = JSON.parse(await run(binary, ['accounts', '--json'], { env }));
|
|
211
|
+
authorization = await collectMagpieAuth(await dataDir(binary), accounts);
|
|
212
|
+
} catch { throw new Error('Unable to export Magpie authorizations'); }
|
|
213
|
+
const payload = Buffer.from(JSON.stringify({ format: 'magpie-sync-v2', passphrase, backup: backup.toString('base64'), authorization }));
|
|
214
|
+
if (payload.length > MAX_BYTES * 2) throw new Error('Magpie sync exceeds size limit');
|
|
215
|
+
return payload;
|
|
178
216
|
});
|
|
179
217
|
}),
|
|
180
218
|
import: (payload) => exclusive(async () => {
|
|
@@ -183,8 +221,9 @@ export function createMagpieSync({
|
|
|
183
221
|
try {
|
|
184
222
|
if (payload.length > MAX_BYTES * 2) throw new Error();
|
|
185
223
|
decoded = JSON.parse(Buffer.from(payload).toString());
|
|
186
|
-
if (
|
|
224
|
+
if (!['magpie-sync-v1', 'magpie-sync-v2'].includes(decoded.format) || !/^[a-f0-9]{64}$/.test(decoded.passphrase)
|
|
187
225
|
|| typeof decoded.backup !== 'string') throw new Error();
|
|
226
|
+
if (decoded.format === 'magpie-sync-v2') validateMagpieAuth(decoded.authorization);
|
|
188
227
|
backup = Buffer.from(decoded.backup, 'base64');
|
|
189
228
|
if (!backup.length || backup.length > MAX_BYTES || JSON.parse(backup.toString()).format !== 'magpie-backup') throw new Error();
|
|
190
229
|
} catch { throw new Error('Invalid Magpie sync payload'); }
|
|
@@ -194,10 +233,43 @@ export function createMagpieSync({
|
|
|
194
233
|
let output;
|
|
195
234
|
try { output = await run(binary, ['restore', file], { env, input: `${decoded.passphrase}\n` }); }
|
|
196
235
|
catch { throw new Error('Magpie restore failed; some configuration may already have been applied. Retry to finish.'); }
|
|
197
|
-
if (/Couldn't add the plugin
|
|
198
|
-
|
|
236
|
+
if (/Couldn't add the plugin/i.test(output)) throw new Error('Magpie restore partially completed: plugin installation failed. Check the target Magpie plugins.');
|
|
237
|
+
if (/Library:.*couldn't/i.test(output)) throw new Error('Magpie restore partially completed: library application failed. Check the target Magpie library.');
|
|
238
|
+
if (/Needs a key/i.test(output)) throw new Error('Magpie restore partially completed: some providers still require API keys.');
|
|
239
|
+
let needsSignIn = /not signed in on this computer/i.test(output);
|
|
240
|
+
if (decoded.format === 'magpie-sync-v2') {
|
|
241
|
+
const dir = await dataDir(binary);
|
|
242
|
+
try {
|
|
243
|
+
await restoreMagpieAuth(dir, decoded.authorization, async (auth) => {
|
|
244
|
+
const credentialFile = path.join(path.dirname(file), 'codex.json');
|
|
245
|
+
try {
|
|
246
|
+
await fs.writeFile(credentialFile, JSON.stringify(auth), { mode: 0o600, flag: 'wx' });
|
|
247
|
+
await run(binary, ['accounts', 'import', 'codex', credentialFile, '--yes'], { env });
|
|
248
|
+
} finally { await fs.rm(credentialFile, { force: true }); }
|
|
249
|
+
});
|
|
250
|
+
const accounts = JSON.parse(await run(binary, ['accounts', '--json'], { env }));
|
|
251
|
+
verifyMagpieAuth(decoded.authorization, accounts);
|
|
252
|
+
const configured = JSON.parse(await fs.readFile(path.join(dir, 'providers.json'), 'utf8').catch((error) => {
|
|
253
|
+
if (error.code === 'ENOENT') return '{"providers":[]}';
|
|
254
|
+
throw error;
|
|
255
|
+
}));
|
|
256
|
+
if (!Array.isArray(configured.providers)) throw new Error();
|
|
257
|
+
const signedIn = new Set(accounts.map((account) => account.agent));
|
|
258
|
+
needsSignIn = decoded.authorization.incomplete || configured.providers.some((provider) =>
|
|
259
|
+
!provider.hidden && provider.id && !['chat', 'responses', 'anthropic', 'gemini'].some((key) => provider[key])
|
|
260
|
+
&& !signedIn.has(provider.id));
|
|
261
|
+
if (/Left as they are/i.test(output)) {
|
|
262
|
+
output = await run(binary, ['restore', file], { env, input: `${decoded.passphrase}\n` });
|
|
263
|
+
if (/Couldn't add the plugin|Needs a key|Library:.*couldn't/i.test(output)) throw new Error();
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
throw new Error('Magpie authorization sync or verification failed; some configuration and accounts may already be applied. Retry to finish.');
|
|
267
|
+
}
|
|
199
268
|
}
|
|
200
|
-
|
|
269
|
+
const magpieWarnings = [];
|
|
270
|
+
if (needsSignIn) magpieWarnings.push('sign_in_required');
|
|
271
|
+
if (/Left as they are/i.test(output)) magpieWarnings.push('agent_settings_skipped');
|
|
272
|
+
return { ok: true, ...(magpieWarnings.length ? { magpieWarnings } : {}) };
|
|
201
273
|
});
|
|
202
274
|
}),
|
|
203
275
|
};
|