fix: agent auth token refresh. (#267)

sweep/improve-logging-information
Zhiming Ma 2023-06-24 21:33:33 +08:00 committed by GitHub
parent 631cff3aed
commit 450bf5eded
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 200 additions and 152 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -136,7 +136,7 @@ declare class TabbyAgent extends EventEmitter implements Agent {
private constructor(); private constructor();
static create(options?: Partial<TabbyAgentOptions>): Promise<TabbyAgent>; static create(options?: Partial<TabbyAgentOptions>): Promise<TabbyAgent>;
private applyConfig; private applyConfig;
private onAuthUpdated; private setupApi;
private changeStatus; private changeStatus;
private callApi; private callApi;
private healthCheck; private healthCheck;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -571,7 +571,7 @@ var ApiService = class {
var CloudApi = class { var CloudApi = class {
constructor(config, HttpRequest = AxiosHttpRequest) { constructor(config, HttpRequest = AxiosHttpRequest) {
this.request = new HttpRequest({ this.request = new HttpRequest({
BASE: config?.BASE, BASE: config?.BASE ?? "https://app.tabbyml.com/api",
VERSION: config?.VERSION ?? "0.0.0", VERSION: config?.VERSION ?? "0.0.0",
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false, WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config?.CREDENTIALS ?? "include", CREDENTIALS: config?.CREDENTIALS ?? "include",
@ -634,8 +634,7 @@ var _Auth = class extends import_events.EventEmitter {
this.jwt = null; this.jwt = null;
this.endpoint = options.endpoint; this.endpoint = options.endpoint;
this.dataStore = options.dataStore || dataStore; this.dataStore = options.dataStore || dataStore;
const authApiBase = "https://app.tabbyml.com/api"; this.authApi = new CloudApi();
this.authApi = new CloudApi({ BASE: authApiBase });
} }
static async create(options) { static async create(options) {
const auth = new _Auth(options); const auth = new _Auth(options);
@ -661,7 +660,7 @@ var _Auth = class extends import_events.EventEmitter {
payload: (0, import_jwt_decode.default)(storedJwt) payload: (0, import_jwt_decode.default)(storedJwt)
}; };
if (jwt.payload.exp * 1e3 - Date.now() < _Auth.tokenStrategy.refresh.beforeExpire) { if (jwt.payload.exp * 1e3 - Date.now() < _Auth.tokenStrategy.refresh.beforeExpire) {
this.jwt = await this.refreshToken(jwt); this.jwt = await this.refreshToken(jwt, _Auth.tokenStrategy.refresh.whenLoaded);
await this.save(); await this.save();
} else { } else {
this.jwt = jwt; this.jwt = jwt;
@ -724,7 +723,7 @@ var _Auth = class extends import_events.EventEmitter {
throw error; throw error;
} }
} }
async refreshToken(jwt, retry = 0) { async refreshToken(jwt, options = { maxTry: 1, retryDelay: 1e3 }, retry = 0) {
try { try {
this.logger.debug({ retry }, "Start to refresh token"); this.logger.debug({ retry }, "Start to refresh token");
const refreshedJwt = await this.authApi.api.deviceTokenRefresh(jwt.token); const refreshedJwt = await this.authApi.api.deviceTokenRefresh(jwt.token);
@ -734,14 +733,14 @@ var _Auth = class extends import_events.EventEmitter {
payload: (0, import_jwt_decode.default)(refreshedJwt.data.jwt) payload: (0, import_jwt_decode.default)(refreshedJwt.data.jwt)
}; };
} catch (error) { } catch (error) {
if (error instanceof ApiError && [401, 403, 405].indexOf(error.status) !== -1) { if (error instanceof ApiError && [400, 401, 403, 405].indexOf(error.status) !== -1) {
this.logger.debug({ error }, "Error when refreshing jwt"); this.logger.debug({ error }, "Error when refreshing jwt");
} else { } else {
this.logger.error({ error }, "Unknown error when refreshing jwt"); this.logger.error({ error }, "Unknown error when refreshing jwt");
if (retry < _Auth.tokenStrategy.refresh.maxTry) { if (retry < options.maxTry) {
await new Promise((resolve2) => setTimeout(resolve2, _Auth.tokenStrategy.refresh.retryDelay)); this.logger.debug(`Retry refreshing jwt after ${options.retryDelay}ms`);
this.logger.debug("Retry refreshing jwt"); await new Promise((resolve2) => setTimeout(resolve2, options.retryDelay));
return this.refreshToken(jwt, retry + 1); return this.refreshToken(jwt, options, retry + 1);
} }
} }
throw { ...error, retry }; throw { ...error, retry };
@ -762,7 +761,7 @@ var _Auth = class extends import_events.EventEmitter {
clearInterval(this.pollingTokenTimer); clearInterval(this.pollingTokenTimer);
this.pollingTokenTimer = null; this.pollingTokenTimer = null;
} catch (error) { } catch (error) {
if (error instanceof ApiError && [401, 403, 405].indexOf(error.status) !== -1) { if (error instanceof ApiError && [400, 401, 403, 405].indexOf(error.status) !== -1) {
this.logger.debug({ error }, "Expected error when polling jwt"); this.logger.debug({ error }, "Expected error when polling jwt");
} else { } else {
this.logger.error({ error }, "Error when polling jwt"); this.logger.error({ error }, "Error when polling jwt");
@ -784,17 +783,20 @@ var _Auth = class extends import_events.EventEmitter {
if (!this.jwt) { if (!this.jwt) {
return null; return null;
} }
const refreshDelay = Math.max( this.refreshTokenTimer = setInterval(async () => {
0, if (this.jwt.payload.exp * 1e3 - Date.now() < _Auth.tokenStrategy.refresh.beforeExpire) {
this.jwt.payload.exp * 1e3 - _Auth.tokenStrategy.refresh.beforeExpire - Date.now() try {
); this.jwt = await this.refreshToken(this.jwt, _Auth.tokenStrategy.refresh.whenScheduled);
this.logger.debug({ refreshDelay }, "Schedule refresh token");
this.refreshTokenTimer = setTimeout(async () => {
this.jwt = await this.refreshToken(this.jwt);
await this.save(); await this.save();
this.scheduleRefreshToken(); this.scheduleRefreshToken();
super.emit("updated", this.jwt); super.emit("updated", this.jwt);
}, refreshDelay); } catch (error) {
this.logger.error({ error }, "Error when refreshing jwt");
}
} else {
this.logger.debug("Check token, still valid");
}
}, _Auth.tokenStrategy.refresh.interval);
} }
}; };
var Auth = _Auth; var Auth = _Auth;
@ -808,13 +810,22 @@ Auth.tokenStrategy = {
// stop polling after trying for 5 min // stop polling after trying for 5 min
}, },
refresh: { refresh: {
// refresh token 30 min before token expires // check token every 15 min, refresh token if it expires in 30 min
// assume a new token expires in 1 day, much longer than 30 min interval: 15 * 60 * 1e3,
beforeExpire: 30 * 60 * 1e3, beforeExpire: 30 * 60 * 1e3,
whenLoaded: {
// after token loaded from data store, refresh token if it is about to expire or has expired
maxTry: 5, maxTry: 5,
// try to refresh token 5 times // keep loading time not too long
retryDelay: 2e3 retryDelay: 1e3
// retry after 2 seconds // retry after 1 seconds
},
whenScheduled: {
// if running until token is about to expire, refresh token as scheduled
maxTry: 60,
retryDelay: 30 * 1e3
// retry after 30 seconds
}
} }
}; };
@ -1045,7 +1056,7 @@ var version = "0.0.1";
var import_uuid = require("uuid"); var import_uuid = require("uuid");
var AnonymousUsageLogger = class { var AnonymousUsageLogger = class {
constructor() { constructor() {
this.anonymousUsageTrackingApi = new CloudApi({ BASE: "https://app.tabbyml.com/api" }); this.anonymousUsageTrackingApi = new CloudApi();
this.logger = rootLogger.child({ component: "AnonymousUsage" }); this.logger = rootLogger.child({ component: "AnonymousUsage" });
this.systemData = { this.systemData = {
agent: `${name}, ${version}`, agent: `${name}, ${version}`,
@ -1128,12 +1139,16 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable; this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable;
if (this.config.server.endpoint !== this.auth?.endpoint) { if (this.config.server.endpoint !== this.auth?.endpoint) {
this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore }); this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore });
this.auth.on("updated", this.onAuthUpdated.bind(this)); this.auth.on("updated", this.setupApi.bind(this));
} }
this.api = new TabbyApi({ BASE: this.config.server.endpoint, TOKEN: this.auth.token }); await this.setupApi();
} }
async onAuthUpdated() { async setupApi() {
this.api = new TabbyApi({ BASE: this.config.server.endpoint, TOKEN: this.auth.token }); this.api = new TabbyApi({
BASE: this.config.server.endpoint.replace(/\/+$/, ""),
// remove trailing slash
TOKEN: this.auth?.token
});
await this.healthCheck(); await this.healthCheck();
} }
changeStatus(status) { changeStatus(status) {
@ -1172,7 +1187,7 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
} }
); );
} }
async healthCheck() { healthCheck() {
return this.callApi(this.api.v1.health, {}).catch(() => { return this.callApi(this.api.v1.health, {}).catch(() => {
}); });
} }
@ -1190,9 +1205,12 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
} }
async initialize(options) { async initialize(options) {
if (options.client) { if (options.client) {
allLoggers.forEach((logger2) => logger2.setBindings && logger2.setBindings({ client: options.client })); allLoggers.forEach((logger2) => logger2.setBindings?.({ client: options.client }));
} }
await this.updateConfig(options.config || {}); if (options.config) {
this.config = (0, import_deepmerge.default)(this.config, options.config);
}
await this.applyConfig();
await this.anonymousUsageLogger.event("AgentInitialized", { await this.anonymousUsageLogger.event("AgentInitialized", {
client: options.client client: options.client
}); });
@ -1208,7 +1226,6 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
this.logger.debug({ event }, "Config updated"); this.logger.debug({ event }, "Config updated");
super.emit("configUpdated", event); super.emit("configUpdated", event);
} }
await this.healthCheck();
return true; return true;
} }
getConfig() { getConfig() {

File diff suppressed because one or more lines are too long

View File

@ -13289,7 +13289,7 @@ var ApiService = class {
var CloudApi = class { var CloudApi = class {
constructor(config2, HttpRequest = AxiosHttpRequest) { constructor(config2, HttpRequest = AxiosHttpRequest) {
this.request = new HttpRequest({ this.request = new HttpRequest({
BASE: config2?.BASE, BASE: config2?.BASE ?? "https://app.tabbyml.com/api",
VERSION: config2?.VERSION ?? "0.0.0", VERSION: config2?.VERSION ?? "0.0.0",
WITH_CREDENTIALS: config2?.WITH_CREDENTIALS ?? false, WITH_CREDENTIALS: config2?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config2?.CREDENTIALS ?? "include", CREDENTIALS: config2?.CREDENTIALS ?? "include",
@ -13360,8 +13360,7 @@ var _Auth = class extends EventEmitter {
this.jwt = null; this.jwt = null;
this.endpoint = options.endpoint; this.endpoint = options.endpoint;
this.dataStore = options.dataStore || dataStore; this.dataStore = options.dataStore || dataStore;
const authApiBase = "https://app.tabbyml.com/api"; this.authApi = new CloudApi();
this.authApi = new CloudApi({ BASE: authApiBase });
} }
static async create(options) { static async create(options) {
const auth = new _Auth(options); const auth = new _Auth(options);
@ -13387,7 +13386,7 @@ var _Auth = class extends EventEmitter {
payload: jwt_decode_esm_default(storedJwt) payload: jwt_decode_esm_default(storedJwt)
}; };
if (jwt.payload.exp * 1e3 - Date.now() < _Auth.tokenStrategy.refresh.beforeExpire) { if (jwt.payload.exp * 1e3 - Date.now() < _Auth.tokenStrategy.refresh.beforeExpire) {
this.jwt = await this.refreshToken(jwt); this.jwt = await this.refreshToken(jwt, _Auth.tokenStrategy.refresh.whenLoaded);
await this.save(); await this.save();
} else { } else {
this.jwt = jwt; this.jwt = jwt;
@ -13450,7 +13449,7 @@ var _Auth = class extends EventEmitter {
throw error; throw error;
} }
} }
async refreshToken(jwt, retry = 0) { async refreshToken(jwt, options = { maxTry: 1, retryDelay: 1e3 }, retry = 0) {
try { try {
this.logger.debug({ retry }, "Start to refresh token"); this.logger.debug({ retry }, "Start to refresh token");
const refreshedJwt = await this.authApi.api.deviceTokenRefresh(jwt.token); const refreshedJwt = await this.authApi.api.deviceTokenRefresh(jwt.token);
@ -13460,14 +13459,14 @@ var _Auth = class extends EventEmitter {
payload: jwt_decode_esm_default(refreshedJwt.data.jwt) payload: jwt_decode_esm_default(refreshedJwt.data.jwt)
}; };
} catch (error) { } catch (error) {
if (error instanceof ApiError && [401, 403, 405].indexOf(error.status) !== -1) { if (error instanceof ApiError && [400, 401, 403, 405].indexOf(error.status) !== -1) {
this.logger.debug({ error }, "Error when refreshing jwt"); this.logger.debug({ error }, "Error when refreshing jwt");
} else { } else {
this.logger.error({ error }, "Unknown error when refreshing jwt"); this.logger.error({ error }, "Unknown error when refreshing jwt");
if (retry < _Auth.tokenStrategy.refresh.maxTry) { if (retry < options.maxTry) {
await new Promise((resolve4) => setTimeout(resolve4, _Auth.tokenStrategy.refresh.retryDelay)); this.logger.debug(`Retry refreshing jwt after ${options.retryDelay}ms`);
this.logger.debug("Retry refreshing jwt"); await new Promise((resolve4) => setTimeout(resolve4, options.retryDelay));
return this.refreshToken(jwt, retry + 1); return this.refreshToken(jwt, options, retry + 1);
} }
} }
throw { ...error, retry }; throw { ...error, retry };
@ -13488,7 +13487,7 @@ var _Auth = class extends EventEmitter {
clearInterval(this.pollingTokenTimer); clearInterval(this.pollingTokenTimer);
this.pollingTokenTimer = null; this.pollingTokenTimer = null;
} catch (error) { } catch (error) {
if (error instanceof ApiError && [401, 403, 405].indexOf(error.status) !== -1) { if (error instanceof ApiError && [400, 401, 403, 405].indexOf(error.status) !== -1) {
this.logger.debug({ error }, "Expected error when polling jwt"); this.logger.debug({ error }, "Expected error when polling jwt");
} else { } else {
this.logger.error({ error }, "Error when polling jwt"); this.logger.error({ error }, "Error when polling jwt");
@ -13510,17 +13509,20 @@ var _Auth = class extends EventEmitter {
if (!this.jwt) { if (!this.jwt) {
return null; return null;
} }
const refreshDelay = Math.max( this.refreshTokenTimer = setInterval(async () => {
0, if (this.jwt.payload.exp * 1e3 - Date.now() < _Auth.tokenStrategy.refresh.beforeExpire) {
this.jwt.payload.exp * 1e3 - _Auth.tokenStrategy.refresh.beforeExpire - Date.now() try {
); this.jwt = await this.refreshToken(this.jwt, _Auth.tokenStrategy.refresh.whenScheduled);
this.logger.debug({ refreshDelay }, "Schedule refresh token");
this.refreshTokenTimer = setTimeout(async () => {
this.jwt = await this.refreshToken(this.jwt);
await this.save(); await this.save();
this.scheduleRefreshToken(); this.scheduleRefreshToken();
super.emit("updated", this.jwt); super.emit("updated", this.jwt);
}, refreshDelay); } catch (error) {
this.logger.error({ error }, "Error when refreshing jwt");
}
} else {
this.logger.debug("Check token, still valid");
}
}, _Auth.tokenStrategy.refresh.interval);
} }
}; };
var Auth = _Auth; var Auth = _Auth;
@ -13534,13 +13536,22 @@ Auth.tokenStrategy = {
// stop polling after trying for 5 min // stop polling after trying for 5 min
}, },
refresh: { refresh: {
// refresh token 30 min before token expires // check token every 15 min, refresh token if it expires in 30 min
// assume a new token expires in 1 day, much longer than 30 min interval: 15 * 60 * 1e3,
beforeExpire: 30 * 60 * 1e3, beforeExpire: 30 * 60 * 1e3,
whenLoaded: {
// after token loaded from data store, refresh token if it is about to expire or has expired
maxTry: 5, maxTry: 5,
// try to refresh token 5 times // keep loading time not too long
retryDelay: 2e3 retryDelay: 1e3
// retry after 2 seconds // retry after 1 seconds
},
whenScheduled: {
// if running until token is about to expire, refresh token as scheduled
maxTry: 60,
retryDelay: 30 * 1e3
// retry after 30 seconds
}
} }
}; };
@ -15125,7 +15136,7 @@ var version3 = "0.0.1";
// src/AnonymousUsageLogger.ts // src/AnonymousUsageLogger.ts
var AnonymousUsageLogger = class { var AnonymousUsageLogger = class {
constructor() { constructor() {
this.anonymousUsageTrackingApi = new CloudApi({ BASE: "https://app.tabbyml.com/api" }); this.anonymousUsageTrackingApi = new CloudApi();
this.logger = rootLogger.child({ component: "AnonymousUsage" }); this.logger = rootLogger.child({ component: "AnonymousUsage" });
this.systemData = { this.systemData = {
agent: `${name2}, ${version3}`, agent: `${name2}, ${version3}`,
@ -15208,12 +15219,16 @@ var _TabbyAgent = class extends EventEmitter {
this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable; this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable;
if (this.config.server.endpoint !== this.auth?.endpoint) { if (this.config.server.endpoint !== this.auth?.endpoint) {
this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore }); this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore });
this.auth.on("updated", this.onAuthUpdated.bind(this)); this.auth.on("updated", this.setupApi.bind(this));
} }
this.api = new TabbyApi({ BASE: this.config.server.endpoint, TOKEN: this.auth.token }); await this.setupApi();
} }
async onAuthUpdated() { async setupApi() {
this.api = new TabbyApi({ BASE: this.config.server.endpoint, TOKEN: this.auth.token }); this.api = new TabbyApi({
BASE: this.config.server.endpoint.replace(/\/+$/, ""),
// remove trailing slash
TOKEN: this.auth?.token
});
await this.healthCheck(); await this.healthCheck();
} }
changeStatus(status) { changeStatus(status) {
@ -15252,7 +15267,7 @@ var _TabbyAgent = class extends EventEmitter {
} }
); );
} }
async healthCheck() { healthCheck() {
return this.callApi(this.api.v1.health, {}).catch(() => { return this.callApi(this.api.v1.health, {}).catch(() => {
}); });
} }
@ -15270,9 +15285,12 @@ var _TabbyAgent = class extends EventEmitter {
} }
async initialize(options) { async initialize(options) {
if (options.client) { if (options.client) {
allLoggers.forEach((logger2) => logger2.setBindings && logger2.setBindings({ client: options.client })); allLoggers.forEach((logger2) => logger2.setBindings?.({ client: options.client }));
} }
await this.updateConfig(options.config || {}); if (options.config) {
this.config = (0, import_deepmerge.default)(this.config, options.config);
}
await this.applyConfig();
await this.anonymousUsageLogger.event("AgentInitialized", { await this.anonymousUsageLogger.event("AgentInitialized", {
client: options.client client: options.client
}); });
@ -15288,7 +15306,6 @@ var _TabbyAgent = class extends EventEmitter {
this.logger.debug({ event }, "Config updated"); this.logger.debug({ event }, "Config updated");
super.emit("configUpdated", event); super.emit("configUpdated", event);
} }
await this.healthCheck();
return true; return true;
} }
getConfig() { getConfig() {

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@ import { rootLogger } from "./logger";
import { dataStore, DataStore } from "./dataStore"; import { dataStore, DataStore } from "./dataStore";
export class AnonymousUsageLogger { export class AnonymousUsageLogger {
private anonymousUsageTrackingApi = new CloudApi({ BASE: "https://app.tabbyml.com/api" }); private anonymousUsageTrackingApi = new CloudApi();
private logger = rootLogger.child({ component: "AnonymousUsage" }); private logger = rootLogger.child({ component: "AnonymousUsage" });
private systemData = { private systemData = {
agent: `${agentName}, ${agentVersion}`, agent: `${agentName}, ${agentVersion}`,

View File

@ -20,11 +20,19 @@ export class Auth extends EventEmitter {
timeout: 5 * 60 * 1000, // stop polling after trying for 5 min timeout: 5 * 60 * 1000, // stop polling after trying for 5 min
}, },
refresh: { refresh: {
// refresh token 30 min before token expires // check token every 15 min, refresh token if it expires in 30 min
// assume a new token expires in 1 day, much longer than 30 min interval: 15 * 60 * 1000,
beforeExpire: 30 * 60 * 1000, beforeExpire: 30 * 60 * 1000,
maxTry: 5, // try to refresh token 5 times whenLoaded: {
retryDelay: 2000, // retry after 2 seconds // after token loaded from data store, refresh token if it is about to expire or has expired
maxTry: 5, // keep loading time not too long
retryDelay: 1000, // retry after 1 seconds
},
whenScheduled: {
// if running until token is about to expire, refresh token as scheduled
maxTry: 60,
retryDelay: 30 * 1000, // retry after 30 seconds
},
}, },
}; };
@ -33,7 +41,7 @@ export class Auth extends EventEmitter {
readonly dataStore: DataStore | null = null; readonly dataStore: DataStore | null = null;
private pollingTokenTimer: ReturnType<typeof setInterval> | null = null; private pollingTokenTimer: ReturnType<typeof setInterval> | null = null;
private stopPollingTokenTimer: ReturnType<typeof setTimeout> | null = null; private stopPollingTokenTimer: ReturnType<typeof setTimeout> | null = null;
private refreshTokenTimer: ReturnType<typeof setTimeout> | null = null; private refreshTokenTimer: ReturnType<typeof setInterval> | null = null;
private authApi: CloudApi | null = null; private authApi: CloudApi | null = null;
private jwt: JWT | null = null; private jwt: JWT | null = null;
@ -47,9 +55,7 @@ export class Auth extends EventEmitter {
super(); super();
this.endpoint = options.endpoint; this.endpoint = options.endpoint;
this.dataStore = options.dataStore || dataStore; this.dataStore = options.dataStore || dataStore;
this.authApi = new CloudApi();
const authApiBase = "https://app.tabbyml.com/api";
this.authApi = new CloudApi({ BASE: authApiBase });
} }
get token(): string | null { get token(): string | null {
@ -73,7 +79,7 @@ export class Auth extends EventEmitter {
}; };
// refresh token if it is about to expire or has expired // refresh token if it is about to expire or has expired
if (jwt.payload.exp * 1000 - Date.now() < Auth.tokenStrategy.refresh.beforeExpire) { if (jwt.payload.exp * 1000 - Date.now() < Auth.tokenStrategy.refresh.beforeExpire) {
this.jwt = await this.refreshToken(jwt); this.jwt = await this.refreshToken(jwt, Auth.tokenStrategy.refresh.whenLoaded);
await this.save(); await this.save();
} else { } else {
this.jwt = jwt; this.jwt = jwt;
@ -137,7 +143,7 @@ export class Auth extends EventEmitter {
} }
} }
private async refreshToken(jwt: JWT, retry = 0): Promise<JWT> { private async refreshToken(jwt: JWT, options = { maxTry: 1, retryDelay: 1000 }, retry = 0): Promise<JWT> {
try { try {
this.logger.debug({ retry }, "Start to refresh token"); this.logger.debug({ retry }, "Start to refresh token");
const refreshedJwt = await this.authApi.api.deviceTokenRefresh(jwt.token); const refreshedJwt = await this.authApi.api.deviceTokenRefresh(jwt.token);
@ -147,15 +153,15 @@ export class Auth extends EventEmitter {
payload: decodeJwt(refreshedJwt.data.jwt), payload: decodeJwt(refreshedJwt.data.jwt),
}; };
} catch (error) { } catch (error) {
if (error instanceof ApiError && [401, 403, 405].indexOf(error.status) !== -1) { if (error instanceof ApiError && [400, 401, 403, 405].indexOf(error.status) !== -1) {
this.logger.debug({ error }, "Error when refreshing jwt"); this.logger.debug({ error }, "Error when refreshing jwt");
} else { } else {
// unknown error, retry a few times // unknown error, retry a few times
this.logger.error({ error }, "Unknown error when refreshing jwt"); this.logger.error({ error }, "Unknown error when refreshing jwt");
if (retry < Auth.tokenStrategy.refresh.maxTry) { if (retry < options.maxTry) {
await new Promise((resolve) => setTimeout(resolve, Auth.tokenStrategy.refresh.retryDelay)); this.logger.debug(`Retry refreshing jwt after ${options.retryDelay}ms`);
this.logger.debug("Retry refreshing jwt"); await new Promise((resolve) => setTimeout(resolve, options.retryDelay));
return this.refreshToken(jwt, retry + 1); return this.refreshToken(jwt, options, retry + 1);
} }
} }
throw { ...error, retry }; throw { ...error, retry };
@ -177,7 +183,7 @@ export class Auth extends EventEmitter {
clearInterval(this.pollingTokenTimer); clearInterval(this.pollingTokenTimer);
this.pollingTokenTimer = null; this.pollingTokenTimer = null;
} catch (error) { } catch (error) {
if (error instanceof ApiError && [401, 403, 405].indexOf(error.status) !== -1) { if (error instanceof ApiError && [400, 401, 403, 405].indexOf(error.status) !== -1) {
this.logger.debug({ error }, "Expected error when polling jwt"); this.logger.debug({ error }, "Expected error when polling jwt");
} else { } else {
// unknown error but still keep polling // unknown error but still keep polling
@ -202,16 +208,19 @@ export class Auth extends EventEmitter {
return null; return null;
} }
const refreshDelay = Math.max( this.refreshTokenTimer = setInterval(async () => {
0, if (this.jwt.payload.exp * 1000 - Date.now() < Auth.tokenStrategy.refresh.beforeExpire) {
this.jwt.payload.exp * 1000 - Auth.tokenStrategy.refresh.beforeExpire - Date.now() try {
); this.jwt = await this.refreshToken(this.jwt, Auth.tokenStrategy.refresh.whenScheduled);
this.logger.debug({ refreshDelay }, "Schedule refresh token");
this.refreshTokenTimer = setTimeout(async () => {
this.jwt = await this.refreshToken(this.jwt);
await this.save(); await this.save();
this.scheduleRefreshToken(); this.scheduleRefreshToken();
super.emit("updated", this.jwt); super.emit("updated", this.jwt);
}, refreshDelay); } catch (error) {
this.logger.error({ error }, "Error when refreshing jwt");
}
} else {
this.logger.debug("Check token, still valid");
}
}, Auth.tokenStrategy.refresh.interval);
} }
} }

View File

@ -64,13 +64,16 @@ export class TabbyAgent extends EventEmitter implements Agent {
this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable; this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable;
if (this.config.server.endpoint !== this.auth?.endpoint) { if (this.config.server.endpoint !== this.auth?.endpoint) {
this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore }); this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore });
this.auth.on("updated", this.onAuthUpdated.bind(this)); this.auth.on("updated", this.setupApi.bind(this));
} }
this.api = new TabbyApi({ BASE: this.config.server.endpoint, TOKEN: this.auth.token }); await this.setupApi();
} }
private async onAuthUpdated() { private async setupApi() {
this.api = new TabbyApi({ BASE: this.config.server.endpoint, TOKEN: this.auth.token }); this.api = new TabbyApi({
BASE: this.config.server.endpoint.replace(/\/+$/, ""), // remove trailing slash
TOKEN: this.auth?.token,
});
await this.healthCheck(); await this.healthCheck();
} }
@ -117,7 +120,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
); );
} }
private async healthCheck(): Promise<any> { private healthCheck(): Promise<any> {
return this.callApi(this.api.v1.health, {}).catch(() => {}); return this.callApi(this.api.v1.health, {}).catch(() => {});
} }
@ -139,9 +142,12 @@ export class TabbyAgent extends EventEmitter implements Agent {
if (options.client) { if (options.client) {
// Client info is only used in logging for now // Client info is only used in logging for now
// `pino.Logger.setBindings` is not present in the browser // `pino.Logger.setBindings` is not present in the browser
allLoggers.forEach((logger) => logger.setBindings && logger.setBindings({ client: options.client })); allLoggers.forEach((logger) => logger.setBindings?.({ client: options.client }));
} }
await this.updateConfig(options.config || {}); if (options.config) {
this.config = deepMerge(this.config, options.config);
}
await this.applyConfig();
await this.anonymousUsageLogger.event("AgentInitialized", { await this.anonymousUsageLogger.event("AgentInitialized", {
client: options.client, client: options.client,
}); });
@ -158,7 +164,6 @@ export class TabbyAgent extends EventEmitter implements Agent {
this.logger.debug({ event }, "Config updated"); this.logger.debug({ event }, "Config updated");
super.emit("configUpdated", event); super.emit("configUpdated", event);
} }
await this.healthCheck();
return true; return true;
} }

View File

@ -11,7 +11,7 @@ export class CloudApi {
constructor(config?: Partial<OpenAPIConfig>, HttpRequest: HttpRequestConstructor = AxiosHttpRequest) { constructor(config?: Partial<OpenAPIConfig>, HttpRequest: HttpRequestConstructor = AxiosHttpRequest) {
this.request = new HttpRequest({ this.request = new HttpRequest({
BASE: config?.BASE, BASE: config?.BASE ?? "https://app.tabbyml.com/api",
VERSION: config?.VERSION ?? "0.0.0", VERSION: config?.VERSION ?? "0.0.0",
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false, WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config?.CREDENTIALS ?? "include", CREDENTIALS: config?.CREDENTIALS ?? "include",