From 71c57ea647fbc376d1207f11d851c09504c9c02d Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 25 Aug 2026 10:37:05 +0900 Subject: [PATCH 1/4] release: v2.32.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f73ed2d0e5..063ecfe73e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.32.0", + "version": "2.32.1", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From ec51e42d745d2645bcb22cb67855fa053ba1778e Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 25 Aug 2026 20:25:22 +0900 Subject: [PATCH 2/4] release: v2.33.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 063ecfe73e..6f8499ffbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.32.1", + "version": "2.33.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From aaa9eaf37058965373dc42d1ca344e987950b6b6 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 2 Sep 2026 18:43:29 +0900 Subject: [PATCH 3/4] fix(release): pass the bump job's permissions through the reusable-workflow call (#3262) Both v2.40.0 release dispatches (33615174183 preview, 33615177849 main) died at startup_failure: a workflow_call cannot grant its callee more than the calling job holds, and dev-version-bump.yml's job declares contents+pull- requests write. #3129 wired the call but never dispatched a release, so this is its first live run. The caller job now declares exactly the callee's two permissions; no other job in release.yml gains anything. Co-authored-by: jun (cherry picked from commit 7ce0ba51834740d7b4d5ec4793f6572d84624409) --- .github/workflows/release.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 458bb67e0a..261aece1d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,6 +67,14 @@ jobs: bump-dev-version: needs: publish if: ${{ inputs.dry-run != true }} + # A reusable-workflow CALL cannot grant the callee more than the calling job holds, + # and GitHub refuses the whole run at startup when the called workflow's own job + # declares permissions the caller did not pass down ("startup_failure", runs + # 33615174183 / 33615177849 — the first dispatches since #3129 wired this call). + # The callee's job declares exactly these two; nothing else in this file gains them. + permissions: + contents: write + pull-requests: write uses: ./.github/workflows/dev-version-bump.yml with: released-version: v${{ inputs.version }} From c344a849868e8be620ab23b111c1b902cda63b97 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Thu, 3 Sep 2026 14:41:50 +0900 Subject: [PATCH 4/4] fix(security): prevent shared-source pairing lockout --- src/server/gui-session.ts | 15 ++++----------- tests/server-management-auth.test.ts | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/server/gui-session.ts b/src/server/gui-session.ts index db1fab549b..fc6fc514b4 100644 --- a/src/server/gui-session.ts +++ b/src/server/gui-session.ts @@ -341,19 +341,12 @@ export function consumeGuiPairingGrant( tailscaleUser: null, browserOrigin, }; - const sourceRecord = attemptContext - ? pairingSourceAttempts.get(state)?.get(pairingSourceKey(context)) - : undefined; - if (sourceRecord && sourceRecord.windowStartedAt + PAIRING_SOURCE_WINDOW_MS > now - && sourceRecord.failures >= PAIRING_SOURCE_FAILURE_LIMIT) { - return { - allowed: false, - retryAfterSeconds: Math.max(1, Math.ceil((sourceRecord.windowStartedAt + PAIRING_SOURCE_WINDOW_MS - now) / 1000)), - reason: "source", - }; - } + // Source throttling is only a cost bound for invalid guesses. Look up the grant + // first so callers sharing a proxy address cannot lock out a valid redemption. const found = findPairingGrant(grant, state); if (!found) { + // Cross-origin browser requests must not create limiter state as a side effect. + if (!isRemoteGuiBrowserOriginAllowed(browserOrigin, config)) return null; const source = recordSourceFailure(state, context, now); return attemptContext && !source.allowed ? source : null; } diff --git a/tests/server-management-auth.test.ts b/tests/server-management-auth.test.ts index c256dede2a..769bb07aeb 100644 --- a/tests/server-management-auth.test.ts +++ b/tests/server-management-auth.test.ts @@ -1094,7 +1094,7 @@ describe("management and data-plane credential separation", () => { )).toBeNull(); }); - test("pairing burns a grant after five failures and rate-limits a source after ten guesses", () => { + test("pairing burns a grant after five failures and rate-limits allowed-origin guesses without locking out valid grants", () => { const config = hubConfig(); const state = initializeManagementAuthState(config); if (!state.available) throw new Error("expected management auth state"); @@ -1127,6 +1127,30 @@ describe("management and data-plane credential separation", () => { } expect(consumeGuiPairingGrant(validOrigin, { grant: `ocx_pair_${"z".repeat(43)}` }, config, state, now + 10, guessContext)) .toMatchObject({ allowed: false, reason: "source" }); + + const redeemable = createGuiPairingGrant("https://dashboard.example.test", config, state, now + 11); + expect(consumeGuiPairingGrant(validOrigin, { grant: redeemable.grant }, config, state, now + 12, guessContext)) + .toMatchObject({ browserOrigin: "https://dashboard.example.test", issuance: "pairing" }); + + const untrustedOriginContext = { ...context, peerAddress: "192.0.2.12" }; + for (let attempt = 1; attempt <= 10; attempt++) { + expect(consumeGuiPairingGrant( + wrongOrigin, + { grant: `ocx_pair_${String(attempt).padStart(43, "b")}` }, + config, + state, + now + attempt, + untrustedOriginContext, + )).toBeNull(); + } + expect(consumeGuiPairingGrant( + validOrigin, + { grant: `ocx_pair_${"y".repeat(43)}` }, + config, + state, + now + 11, + untrustedOriginContext, + )).toBeNull(); }); test("self logout revokes only the current GUI session and admin credentials get 403", async () => {