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();
static create(options?: Partial<TabbyAgentOptions>): Promise<TabbyAgent>;
private applyConfig;
private onAuthUpdated;
private setupApi;
private changeStatus;
private callApi;
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 {
constructor(config, HttpRequest = AxiosHttpRequest) {
this.request = new HttpRequest({
BASE: config?.BASE,
BASE: config?.BASE ?? "https://app.tabbyml.com/api",
VERSION: config?.VERSION ?? "0.0.0",
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config?.CREDENTIALS ?? "include",
@ -634,8 +634,7 @@ var _Auth = class extends import_events.EventEmitter {
this.jwt = null;
this.endpoint = options.endpoint;
this.dataStore = options.dataStore || dataStore;
const authApiBase = "https://app.tabbyml.com/api";
this.authApi = new CloudApi({ BASE: authApiBase });
this.authApi = new CloudApi();
}
static async create(options) {
const auth = new _Auth(options);
@ -661,7 +660,7 @@ var _Auth = class extends import_events.EventEmitter {
payload: (0, import_jwt_decode.default)(storedJwt)
};
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();
} else {
this.jwt = jwt;
@ -724,7 +723,7 @@ var _Auth = class extends import_events.EventEmitter {
throw error;
}
}
async refreshToken(jwt, retry = 0) {
async refreshToken(jwt, options = { maxTry: 1, retryDelay: 1e3 }, retry = 0) {
try {
this.logger.debug({ retry }, "Start to refresh 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)
};
} 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");
} else {
this.logger.error({ error }, "Unknown error when refreshing jwt");
if (retry < _Auth.tokenStrategy.refresh.maxTry) {
await new Promise((resolve2) => setTimeout(resolve2, _Auth.tokenStrategy.refresh.retryDelay));
this.logger.debug("Retry refreshing jwt");
return this.refreshToken(jwt, retry + 1);
if (retry < options.maxTry) {
this.logger.debug(`Retry refreshing jwt after ${options.retryDelay}ms`);
await new Promise((resolve2) => setTimeout(resolve2, options.retryDelay));
return this.refreshToken(jwt, options, retry + 1);
}
}
throw { ...error, retry };
@ -762,7 +761,7 @@ var _Auth = class extends import_events.EventEmitter {
clearInterval(this.pollingTokenTimer);
this.pollingTokenTimer = null;
} 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");
} else {
this.logger.error({ error }, "Error when polling jwt");
@ -784,17 +783,20 @@ var _Auth = class extends import_events.EventEmitter {
if (!this.jwt) {
return null;
}
const refreshDelay = Math.max(
0,
this.jwt.payload.exp * 1e3 - _Auth.tokenStrategy.refresh.beforeExpire - Date.now()
);
this.logger.debug({ refreshDelay }, "Schedule refresh token");
this.refreshTokenTimer = setTimeout(async () => {
this.jwt = await this.refreshToken(this.jwt);
this.refreshTokenTimer = setInterval(async () => {
if (this.jwt.payload.exp * 1e3 - Date.now() < _Auth.tokenStrategy.refresh.beforeExpire) {
try {
this.jwt = await this.refreshToken(this.jwt, _Auth.tokenStrategy.refresh.whenScheduled);
await this.save();
this.scheduleRefreshToken();
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;
@ -808,13 +810,22 @@ Auth.tokenStrategy = {
// stop polling after trying for 5 min
},
refresh: {
// refresh token 30 min before token expires
// assume a new token expires in 1 day, much longer than 30 min
// check token every 15 min, refresh token if it expires in 30 min
interval: 15 * 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,
// try to refresh token 5 times
retryDelay: 2e3
// retry after 2 seconds
// keep loading time not too long
retryDelay: 1e3
// 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 AnonymousUsageLogger = class {
constructor() {
this.anonymousUsageTrackingApi = new CloudApi({ BASE: "https://app.tabbyml.com/api" });
this.anonymousUsageTrackingApi = new CloudApi();
this.logger = rootLogger.child({ component: "AnonymousUsage" });
this.systemData = {
agent: `${name}, ${version}`,
@ -1128,12 +1139,16 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable;
if (this.config.server.endpoint !== this.auth?.endpoint) {
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() {
this.api = new TabbyApi({ BASE: this.config.server.endpoint, TOKEN: this.auth.token });
async setupApi() {
this.api = new TabbyApi({
BASE: this.config.server.endpoint.replace(/\/+$/, ""),
// remove trailing slash
TOKEN: this.auth?.token
});
await this.healthCheck();
}
changeStatus(status) {
@ -1172,7 +1187,7 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
}
);
}
async healthCheck() {
healthCheck() {
return this.callApi(this.api.v1.health, {}).catch(() => {
});
}
@ -1190,9 +1205,12 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
}
async initialize(options) {
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", {
client: options.client
});
@ -1208,7 +1226,6 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
this.logger.debug({ event }, "Config updated");
super.emit("configUpdated", event);
}
await this.healthCheck();
return true;
}
getConfig() {

File diff suppressed because one or more lines are too long

View File

@ -13289,7 +13289,7 @@ var ApiService = class {
var CloudApi = class {
constructor(config2, HttpRequest = AxiosHttpRequest) {
this.request = new HttpRequest({
BASE: config2?.BASE,
BASE: config2?.BASE ?? "https://app.tabbyml.com/api",
VERSION: config2?.VERSION ?? "0.0.0",
WITH_CREDENTIALS: config2?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config2?.CREDENTIALS ?? "include",
@ -13360,8 +13360,7 @@ var _Auth = class extends EventEmitter {
this.jwt = null;
this.endpoint = options.endpoint;
this.dataStore = options.dataStore || dataStore;
const authApiBase = "https://app.tabbyml.com/api";
this.authApi = new CloudApi({ BASE: authApiBase });
this.authApi = new CloudApi();
}
static async create(options) {
const auth = new _Auth(options);
@ -13387,7 +13386,7 @@ var _Auth = class extends EventEmitter {
payload: jwt_decode_esm_default(storedJwt)
};
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();
} else {
this.jwt = jwt;
@ -13450,7 +13449,7 @@ var _Auth = class extends EventEmitter {
throw error;
}
}
async refreshToken(jwt, retry = 0) {
async refreshToken(jwt, options = { maxTry: 1, retryDelay: 1e3 }, retry = 0) {
try {
this.logger.debug({ retry }, "Start to refresh 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)
};
} 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");
} else {
this.logger.error({ error }, "Unknown error when refreshing jwt");
if (retry < _Auth.tokenStrategy.refresh.maxTry) {
await new Promise((resolve4) => setTimeout(resolve4, _Auth.tokenStrategy.refresh.retryDelay));
this.logger.debug("Retry refreshing jwt");
return this.refreshToken(jwt, retry + 1);
if (retry < options.maxTry) {
this.logger.debug(`Retry refreshing jwt after ${options.retryDelay}ms`);
await new Promise((resolve4) => setTimeout(resolve4, options.retryDelay));
return this.refreshToken(jwt, options, retry + 1);
}
}
throw { ...error, retry };
@ -13488,7 +13487,7 @@ var _Auth = class extends EventEmitter {
clearInterval(this.pollingTokenTimer);
this.pollingTokenTimer = null;
} 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");
} else {
this.logger.error({ error }, "Error when polling jwt");
@ -13510,17 +13509,20 @@ var _Auth = class extends EventEmitter {
if (!this.jwt) {
return null;
}
const refreshDelay = Math.max(
0,
this.jwt.payload.exp * 1e3 - _Auth.tokenStrategy.refresh.beforeExpire - Date.now()
);
this.logger.debug({ refreshDelay }, "Schedule refresh token");
this.refreshTokenTimer = setTimeout(async () => {
this.jwt = await this.refreshToken(this.jwt);
this.refreshTokenTimer = setInterval(async () => {
if (this.jwt.payload.exp * 1e3 - Date.now() < _Auth.tokenStrategy.refresh.beforeExpire) {
try {
this.jwt = await this.refreshToken(this.jwt, _Auth.tokenStrategy.refresh.whenScheduled);
await this.save();
this.scheduleRefreshToken();
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;
@ -13534,13 +13536,22 @@ Auth.tokenStrategy = {
// stop polling after trying for 5 min
},
refresh: {
// refresh token 30 min before token expires
// assume a new token expires in 1 day, much longer than 30 min
// check token every 15 min, refresh token if it expires in 30 min
interval: 15 * 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,
// try to refresh token 5 times
retryDelay: 2e3
// retry after 2 seconds
// keep loading time not too long
retryDelay: 1e3
// 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
var AnonymousUsageLogger = class {
constructor() {
this.anonymousUsageTrackingApi = new CloudApi({ BASE: "https://app.tabbyml.com/api" });
this.anonymousUsageTrackingApi = new CloudApi();
this.logger = rootLogger.child({ component: "AnonymousUsage" });
this.systemData = {
agent: `${name2}, ${version3}`,
@ -15208,12 +15219,16 @@ var _TabbyAgent = class extends EventEmitter {
this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable;
if (this.config.server.endpoint !== this.auth?.endpoint) {
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() {
this.api = new TabbyApi({ BASE: this.config.server.endpoint, TOKEN: this.auth.token });
async setupApi() {
this.api = new TabbyApi({
BASE: this.config.server.endpoint.replace(/\/+$/, ""),
// remove trailing slash
TOKEN: this.auth?.token
});
await this.healthCheck();
}
changeStatus(status) {
@ -15252,7 +15267,7 @@ var _TabbyAgent = class extends EventEmitter {
}
);
}
async healthCheck() {
healthCheck() {
return this.callApi(this.api.v1.health, {}).catch(() => {
});
}
@ -15270,9 +15285,12 @@ var _TabbyAgent = class extends EventEmitter {
}
async initialize(options) {
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", {
client: options.client
});
@ -15288,7 +15306,6 @@ var _TabbyAgent = class extends EventEmitter {
this.logger.debug({ event }, "Config updated");
super.emit("configUpdated", event);
}
await this.healthCheck();
return true;
}
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";
export class AnonymousUsageLogger {
private anonymousUsageTrackingApi = new CloudApi({ BASE: "https://app.tabbyml.com/api" });
private anonymousUsageTrackingApi = new CloudApi();
private logger = rootLogger.child({ component: "AnonymousUsage" });
private systemData = {
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
},
refresh: {
// refresh token 30 min before token expires
// assume a new token expires in 1 day, much longer than 30 min
// check token every 15 min, refresh token if it expires in 30 min
interval: 15 * 60 * 1000,
beforeExpire: 30 * 60 * 1000,
maxTry: 5, // try to refresh token 5 times
retryDelay: 2000, // retry after 2 seconds
whenLoaded: {
// 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;
private pollingTokenTimer: ReturnType<typeof setInterval> | 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 jwt: JWT | null = null;
@ -47,9 +55,7 @@ export class Auth extends EventEmitter {
super();
this.endpoint = options.endpoint;
this.dataStore = options.dataStore || dataStore;
const authApiBase = "https://app.tabbyml.com/api";
this.authApi = new CloudApi({ BASE: authApiBase });
this.authApi = new CloudApi();
}
get token(): string | null {
@ -73,7 +79,7 @@ export class Auth extends EventEmitter {
};
// refresh token if it is about to expire or has expired
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();
} else {
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 {
this.logger.debug({ retry }, "Start to refresh token");
const refreshedJwt = await this.authApi.api.deviceTokenRefresh(jwt.token);
@ -147,15 +153,15 @@ export class Auth extends EventEmitter {
payload: decodeJwt(refreshedJwt.data.jwt),
};
} 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");
} else {
// unknown error, retry a few times
this.logger.error({ error }, "Unknown error when refreshing jwt");
if (retry < Auth.tokenStrategy.refresh.maxTry) {
await new Promise((resolve) => setTimeout(resolve, Auth.tokenStrategy.refresh.retryDelay));
this.logger.debug("Retry refreshing jwt");
return this.refreshToken(jwt, retry + 1);
if (retry < options.maxTry) {
this.logger.debug(`Retry refreshing jwt after ${options.retryDelay}ms`);
await new Promise((resolve) => setTimeout(resolve, options.retryDelay));
return this.refreshToken(jwt, options, retry + 1);
}
}
throw { ...error, retry };
@ -177,7 +183,7 @@ export class Auth extends EventEmitter {
clearInterval(this.pollingTokenTimer);
this.pollingTokenTimer = null;
} 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");
} else {
// unknown error but still keep polling
@ -202,16 +208,19 @@ export class Auth extends EventEmitter {
return null;
}
const refreshDelay = Math.max(
0,
this.jwt.payload.exp * 1000 - Auth.tokenStrategy.refresh.beforeExpire - Date.now()
);
this.logger.debug({ refreshDelay }, "Schedule refresh token");
this.refreshTokenTimer = setTimeout(async () => {
this.jwt = await this.refreshToken(this.jwt);
this.refreshTokenTimer = setInterval(async () => {
if (this.jwt.payload.exp * 1000 - Date.now() < Auth.tokenStrategy.refresh.beforeExpire) {
try {
this.jwt = await this.refreshToken(this.jwt, Auth.tokenStrategy.refresh.whenScheduled);
await this.save();
this.scheduleRefreshToken();
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;
if (this.config.server.endpoint !== this.auth?.endpoint) {
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() {
this.api = new TabbyApi({ BASE: this.config.server.endpoint, TOKEN: this.auth.token });
private async setupApi() {
this.api = new TabbyApi({
BASE: this.config.server.endpoint.replace(/\/+$/, ""), // remove trailing slash
TOKEN: this.auth?.token,
});
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(() => {});
}
@ -139,9 +142,12 @@ export class TabbyAgent extends EventEmitter implements Agent {
if (options.client) {
// Client info is only used in logging for now
// `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", {
client: options.client,
});
@ -158,7 +164,6 @@ export class TabbyAgent extends EventEmitter implements Agent {
this.logger.debug({ event }, "Config updated");
super.emit("configUpdated", event);
}
await this.healthCheck();
return true;
}

View File

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