mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
fix: discard obsolete quota recovery after reconnect
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
0ae6037cf2
commit
2d56b73b75
2 files changed
+16
-4
No files matched your search
@@ -166,7 +166,7 @@ describe("managed AI connections", () => {
|
||||
} finally { interrupted.mockRestore(); request.mockRestore(); }
|
||||
});
|
||||
|
||||
it.each(["reconnected", "revoked"] as const)("does not apply a pending refresh over a %s identity", async (change) => {
|
||||
it.each(["reconnected", "expired_reconnect", "revoked"] as const)("does not apply a pending refresh over a %s identity", async (change) => {
|
||||
const owner = `quota-replay-${change}`;
|
||||
await db.insert(companyMemberships).values({ companyId, principalId: owner, principalType: "user", status: "active", membershipRole: "member" });
|
||||
const original = JSON.stringify({ tokens: { access_token: "expired", refresh_token: "single-use", account_id: owner } });
|
||||
@@ -182,12 +182,21 @@ describe("managed AI connections", () => {
|
||||
expect(await service.refreshQuotaCredential(row, original, new AbortController().signal)).toBe("new-authorized-identity");
|
||||
expect(await service.credential(row)).toBe("new-authorized-identity");
|
||||
expect(await journal.read()).toBeNull();
|
||||
} else if (change === "expired_reconnect") {
|
||||
const reconnected = JSON.stringify({ tokens: { access_token: "expired-new-account", refresh_token: "new-account-refresh", account_id: owner } });
|
||||
await secretService(db).rotate(ref.secretId, { value: reconnected });
|
||||
request.mockResolvedValueOnce(Response.json({ access_token: "new-account-fresh", refresh_token: "new-account-replacement" }));
|
||||
await service.refreshQuotaCredential(row, reconnected, new AbortController().signal);
|
||||
expect(request).toHaveBeenCalledOnce();
|
||||
expect(JSON.parse(String(request.mock.calls[0][1]?.body))).toMatchObject({ refresh_token: "new-account-refresh" });
|
||||
expect(JSON.parse(await service.credential(row))).toMatchObject({ tokens: { access_token: "new-account-fresh", refresh_token: "new-account-replacement" } });
|
||||
expect(await journal.read()).toBeNull();
|
||||
} else {
|
||||
await db.update(connectionGrants).set({ status: "revoked" }).where(eq(connectionGrants.id, row.grant.id));
|
||||
await expect(service.refreshQuotaCredential(row, original, new AbortController().signal)).rejects.toThrow("credentials_unavailable");
|
||||
expect(await journal.read()).not.toBeNull();
|
||||
}
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
if (change !== "expired_reconnect") expect(request).not.toHaveBeenCalled();
|
||||
} finally { request.mockRestore(); await journal.clear(); }
|
||||
});
|
||||
|
||||
|
||||
@@ -883,9 +883,12 @@ export function aiConnectionService(db: Db) {
|
||||
signal.throwIfAborted();
|
||||
// Another poll, an agent, or reconnect already supplied a newer credential.
|
||||
if (current !== failedValue) return current;
|
||||
const pending = await recovery.read();
|
||||
let pending = await recovery.read();
|
||||
if (pending && (pending.secretId !== ref.secretId || pending.connectionId !== row.connection.id || pending.baseHash !== quotaCredentialHash(current))) {
|
||||
throw new Error("credentials_unavailable");
|
||||
// Reconnect may reuse this grant. Once the current authorized token
|
||||
// itself expires, an older journal must not prevent refreshing it.
|
||||
await recovery.clear();
|
||||
pending = null;
|
||||
}
|
||||
// An active provider may rotate this single-use token itself. Defer polling
|
||||
// until its credential write-back instead of invalidating its live copy.
|
||||
|
||||
Reference in new issue
Block a user