refactor(qoder): mirror Kiro's OAuth service layout

Move device-flow / poll / userinfo / parseExpiry from src/lib/qoder/auth.js
into a QoderService class at src/lib/oauth/services/qoder.js, matching how
KiroService is organized. Also re-add the QoderService re-export from
services/index.js.

The split now mirrors Kiro:
  src/lib/oauth/services/qoder.js    OAuth flow            (was auth.js)
  src/lib/qoder/cosy.js              Per-request signing   (unchanged)
  src/lib/qoder/encoding.js          WAF-bypass body       (unchanged)
  src/lib/qoder/constants.js         Endpoints + model map (unchanged)

Behavior is unchanged — same functions, same signatures, just relocated
into a class so the import path lines up with `import { QoderService } from
"@/lib/oauth/services"` like every other OAuth provider. parseExpiry is now
a static method so callers and tests can use it without instantiating.

42 tests still pass; build still clean.
This commit is contained in:
Simon Shi authored and decolua committed 2026-05-29 17:36:27 +07:00
1 parent 935462ce8f
commit 69bc71cf11
5 files changed
+236 -220

No files matched your search

+11 -9
View File
@@ -605,8 +605,8 @@ const PROVIDERS = {
// locally, the user lands on qoder.com/device/selectAccounts in the
// browser, and we poll openapi.qoder.sh until a `dt-...` token appears.
requestDeviceCode: async (config) => {
const { initiateDeviceFlow } = await import("@/lib/qoder/auth");
const flow = initiateDeviceFlow();
const { QoderService } = await import("@/lib/oauth/services/qoder");
const flow = new QoderService().initiateDeviceFlow();
// Match the device_code shape the rest of the OAuthModal expects
// (device_code, user_code, verification_uri[_complete], interval).
// The poll endpoint identifies us by nonce+verifier, not by a
@@ -626,7 +626,8 @@ const PROVIDERS = {
};
},
pollToken: async (config, deviceCode, codeVerifier, extraData) => {
const { pollDeviceToken, fetchUserInfo } = await import("@/lib/qoder/auth");
const { QoderService } = await import("@/lib/oauth/services/qoder");
const svc = new QoderService();
const nonce = deviceCode || extraData?._qoderNonce;
const verifier = codeVerifier || extraData?._qoderVerifier;
if (!nonce || !verifier) {
@@ -637,7 +638,7 @@ const PROVIDERS = {
}
let result;
try {
result = await pollDeviceToken({ nonce, codeVerifier: verifier });
result = await svc.pollDeviceToken({ nonce, codeVerifier: verifier });
} catch (err) {
return {
ok: false,
@@ -648,11 +649,12 @@ const PROVIDERS = {
return { ok: false, data: { error: "authorization_pending" } };
}
// Best-effort profile lookup so we have a name/email to display.
const userInfo = await fetchUserInfo(result.accessToken);
// expireTime is a Unix-ms timestamp from parseExpiry, which already
// falls back to "now + 30 days" when the upstream omits expiry. Floor
// to a sane minimum (1 day) so a stale or skewed upstream timestamp
// doesn't truncate the stored token below something useful.
const userInfo = await svc.fetchUserInfo(result.accessToken);
// expireTime is a Unix-ms timestamp from QoderService.parseExpiry,
// which already falls back to "now + 30 days" when the upstream
// omits expiry. Floor to a sane minimum (1 day) so a stale or
// skewed upstream timestamp doesn't truncate the stored token below
// something useful.
const minSeconds = 24 * 60 * 60;
const remainingSeconds = Math.floor((result.expireTime - Date.now()) / 1000);
const expiresIn = Math.max(minSeconds, remainingSeconds);