fix: agent auth token refresh. (#267)
parent
631cff3aed
commit
450bf5eded
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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}`,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue