Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **Provider connections:** keep `tokenExpiresAt` when a connection is created. The create-path allowlist omitted it, so every insert stored NULL and the dashboard token badge could read a fresh connection as expired until its first background refresh ([#11368](https://github.com/diegosouzapw/OmniRoute/pull/11368)).
4 changes: 4 additions & 0 deletions src/lib/db/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,10 @@ export async function createProviderConnection(data: JsonRecord) {
"accessToken",
"refreshToken",
"expiresAt",
// #5326's payload sets this and _insertConnectionRow binds it, but it was
// missing from this allowlist — so every created row stored NULL however good
// the payload was. The update path already carries it (`data.tokenExpiresAt`).
"tokenExpiresAt",
"tokenType",
"scope",
"idToken",
Expand Down
52 changes: 47 additions & 5 deletions tests/unit/oauth-connection-tokenexpiresat-5326.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import { buildOAuthConnectionCreatePayload } from "../../src/lib/oauth/connectionPersistence.ts";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-tokenexpiresat-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "tokenexpiresat-test-secret";

const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const { buildOAuthConnectionCreatePayload } =
await import("../../src/lib/oauth/connectionPersistence.ts");

test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});

// Regression for #5326: a freshly created OAuth connection (e.g. antigravity) used
// to persist only `expiresAt`, leaving `tokenExpiresAt` null. The dashboard token
Expand Down Expand Up @@ -32,12 +47,39 @@ test("buildOAuthConnectionCreatePayload mirrors expiresAt into tokenExpiresAt (#
});

test("buildOAuthConnectionCreatePayload keeps tokenExpiresAt null when expiry is unknown", () => {
const payload = buildOAuthConnectionCreatePayload("antigravity", { accessToken: "at-456" }, null);

assert.equal(payload.expiresAt, null);
assert.equal(payload.tokenExpiresAt, null);
});

// The two cases above assert the payload object only, which is why the create path
// could drop the field for as long as it did. `createProviderConnection` copies
// optional fields through an allowlist, and `tokenExpiresAt` was not on it, so the
// insert bound NULL however good the payload was. Persist and read it back.
test("a created connection keeps tokenExpiresAt through the database", async () => {
core.resetDbInstance();
const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
const payload = buildOAuthConnectionCreatePayload(
"antigravity",
{ accessToken: "at-456" },
null
{
accessToken: "at-789",
refreshToken: "rt-789",
email: "roundtrip@example.com",
expiresIn: 3600,
},
expiresAt
);
assert.equal(payload.tokenExpiresAt, expiresAt, "precondition: the payload carries it");

assert.equal(payload.expiresAt, null);
assert.equal(payload.tokenExpiresAt, null);
const created = await providersDb.createProviderConnection(payload);
const readBack = (await providersDb.getProviderConnections({})).find(
(c: { id: string }) => c.id === created.id
);

assert.ok(readBack, "the connection was persisted");
assert.equal(readBack.expiresAt, expiresAt);
// The dashboard badge and tokenHealthCheck both prefer tokenExpiresAt and fall back
// to expiresAt only when it is null, so a NULL here is a false "Token Expired".
assert.equal(readBack.tokenExpiresAt, expiresAt);
});
Loading