mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
* feat(oauth): accept 9router camelCase Codex export in bulk import (#6665) * fix(changelog): restore CHANGELOG bullets eaten by release sync * fix(changelog): re-restore CHANGELOG bullet after further release sync * fix(changelog): re-restore CHANGELOG bullet after further release sync * fix(changelog): re-restore #6697 bullet after release sync (#6678 landed) * fix(changelog): re-restore CHANGELOG bullet after further release sync * fix(changelog): re-restore #6697 bullet after #6700 release sync * fix(changelog): re-restore #6697 bullet after release sync Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(changelog): restore #6126 bullet eaten by ancestry merge; re-insert only #6697's own * chore(sync): merge release tip + restore own CHANGELOG bullet Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(sync): merge release tip + restore own CHANGELOG bullet * chore(changelog): re-sync after release merge — preserve sibling bullets Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
e310aa7e41
commit
33786137d1
@@ -8,6 +8,8 @@
|
||||
|
||||
_Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._
|
||||
|
||||
- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten).
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625)
|
||||
|
||||
@@ -138,6 +138,36 @@ function unwrapCodexAuthJson(rec: Record<string, unknown>): Record<string, unkno
|
||||
return { ...rec, ...tokens };
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the camelCase field names used by 9router's Codex account export
|
||||
* (`accessToken`, `refreshToken`, `idToken`, `expiresAt`, and a nested
|
||||
* `providerSpecificData` block) onto the snake_case keys the rest of the
|
||||
* normalizer already understands (#6665). A snake_case key is only filled from
|
||||
* its camelCase alias when it is absent, so a snake_case or mixed export keeps
|
||||
* working unchanged.
|
||||
*/
|
||||
function applyCamelCaseAliases(
|
||||
rec: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = { ...rec };
|
||||
const fillFrom = (snake: string, value: unknown) => {
|
||||
if (out[snake] === undefined && typeof value === "string" && value) {
|
||||
out[snake] = value;
|
||||
}
|
||||
};
|
||||
fillFrom("access_token", rec.accessToken);
|
||||
fillFrom("refresh_token", rec.refreshToken);
|
||||
fillFrom("id_token", rec.idToken);
|
||||
fillFrom("expired", rec.expiresAt);
|
||||
const psd = rec.providerSpecificData;
|
||||
if (psd && typeof psd === "object" && !Array.isArray(psd)) {
|
||||
const block = psd as Record<string, unknown>;
|
||||
fillFrom("account_id", block.chatgptAccountId);
|
||||
fillFrom("chatgpt_plan_type", block.chatgptPlanType);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -151,7 +181,9 @@ export function normalizeCodexImportRecord(input: unknown): NormalizeResult {
|
||||
return { ok: false, error: "Record is not an object" };
|
||||
}
|
||||
|
||||
const rec = unwrapCodexAuthJson(input as Record<string, unknown>);
|
||||
const rec = applyCamelCaseAliases(
|
||||
unwrapCodexAuthJson(input as Record<string, unknown>),
|
||||
);
|
||||
|
||||
// Allow type field to be missing or "codex"; reject anything else explicitly so
|
||||
// users don't accidentally import claude/gemini exports through this path.
|
||||
@@ -181,7 +213,10 @@ export function normalizeCodexImportRecord(input: unknown): NormalizeResult {
|
||||
fromJwt.chatgptAccountId,
|
||||
rec.account_id as string | undefined,
|
||||
);
|
||||
const chatgptPlanType = pickString(fromJwt.chatgptPlanType);
|
||||
const chatgptPlanType = pickString(
|
||||
fromJwt.chatgptPlanType,
|
||||
rec.chatgpt_plan_type as string | undefined,
|
||||
);
|
||||
|
||||
const expiresAt =
|
||||
parseExpiry(rec.expired) ?? parseAccessTokenExp(accessToken);
|
||||
|
||||
@@ -243,3 +243,86 @@ describe("flattenCodexImportPayload", () => {
|
||||
assert.equal(flattenCodexImportPayload(null).ok, false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── 9router camelCase export (#6665) ────────────────────────────────────────────
|
||||
|
||||
describe("9router camelCase Codex export (#6665)", () => {
|
||||
// The exact shape a 9router Codex account export produces (camelCase fields +
|
||||
// nested providerSpecificData), as pasted in issue #6665.
|
||||
const NINEROUTER_RECORD = {
|
||||
accessToken: "access-9r",
|
||||
refreshToken: "refresh-9r",
|
||||
idToken: makeIdToken({
|
||||
email: "jwt-9r@example.com",
|
||||
"https://api.openai.com/auth": {
|
||||
chatgpt_account_id: "acct-jwt-9r",
|
||||
chatgpt_plan_type: "plus",
|
||||
},
|
||||
}),
|
||||
email: "top-9r@example.com",
|
||||
expiresAt: "2026-07-17T13:18:27.000Z",
|
||||
expiresIn: 849516,
|
||||
providerSpecificData: {
|
||||
chatgptAccountId: "acct-psd-9r",
|
||||
chatgptPlanType: "pro",
|
||||
},
|
||||
testStatus: "active",
|
||||
isActive: true,
|
||||
lastRefreshAt: "2026-07-07T17:19:51.150Z",
|
||||
};
|
||||
|
||||
test("normalizes a 9router camelCase record", () => {
|
||||
const result = normalizeCodexImportRecord(NINEROUTER_RECORD);
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
const { payload } = result;
|
||||
assert.equal(payload.accessToken, "access-9r");
|
||||
assert.equal(payload.refreshToken, "refresh-9r");
|
||||
assert.equal(payload.idToken, NINEROUTER_RECORD.idToken);
|
||||
// JWT-derived account info wins over the pre-supplied providerSpecificData.
|
||||
assert.equal(payload.email, "jwt-9r@example.com");
|
||||
assert.deepEqual(payload.providerSpecificData, {
|
||||
chatgptAccountId: "acct-jwt-9r",
|
||||
chatgptPlanType: "plus",
|
||||
});
|
||||
// camelCase `expiresAt` is honored as the expiry source.
|
||||
assert.equal(Date.parse(payload.expiresAt), Date.parse(NINEROUTER_RECORD.expiresAt));
|
||||
});
|
||||
|
||||
test("uses pre-supplied providerSpecificData when there is no id_token", () => {
|
||||
const result = normalizeCodexImportRecord({
|
||||
accessToken: "a",
|
||||
refreshToken: "r",
|
||||
email: "no-jwt-9r@example.com",
|
||||
providerSpecificData: { chatgptAccountId: "acct-psd", chatgptPlanType: "team" },
|
||||
});
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
assert.equal(result.payload.email, "no-jwt-9r@example.com");
|
||||
assert.deepEqual(result.payload.providerSpecificData, {
|
||||
chatgptAccountId: "acct-psd",
|
||||
chatgptPlanType: "team",
|
||||
});
|
||||
});
|
||||
|
||||
test("an explicit snake_case field is NOT overridden by a camelCase alias", () => {
|
||||
const result = normalizeCodexImportRecord({
|
||||
access_token: "snake-wins",
|
||||
accessToken: "camel-loses",
|
||||
refresh_token: "r",
|
||||
email: "mixed@example.com",
|
||||
});
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
assert.equal(result.payload.accessToken, "snake-wins");
|
||||
});
|
||||
|
||||
test("a full 9router {accounts:[...]} export flattens to its records", () => {
|
||||
const flat = flattenCodexImportPayload([NINEROUTER_RECORD]);
|
||||
assert.equal(flat.ok, true);
|
||||
if (!flat.ok) return;
|
||||
assert.equal(flat.records.length, 1);
|
||||
const norm = normalizeCodexImportRecord(flat.records[0]);
|
||||
assert.equal(norm.ok, true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user