feat: agent auth support to refresh token. (#262)
parent
b6845ddac0
commit
1752f1555b
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -543,6 +543,18 @@ var ApiService = class {
|
|||
query
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param token
|
||||
* @returns DeviceTokenRefreshResponse Success
|
||||
* @throws ApiError
|
||||
*/
|
||||
deviceTokenRefresh(token) {
|
||||
return this.httpRequest.request({
|
||||
method: "POST",
|
||||
url: "/device-token/refresh",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param body object for anonymous usage tracking
|
||||
*/
|
||||
|
|
@ -559,7 +571,7 @@ var ApiService = class {
|
|||
var CloudApi = class {
|
||||
constructor(config, HttpRequest = AxiosHttpRequest) {
|
||||
this.request = new HttpRequest({
|
||||
BASE: config?.BASE ?? "https://tabbyml.app.tabbyml.com/tabby",
|
||||
BASE: config?.BASE,
|
||||
VERSION: config?.VERSION ?? "0.0.0",
|
||||
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
|
||||
CREDENTIALS: config?.CREDENTIALS ?? "include",
|
||||
|
|
@ -613,10 +625,10 @@ rootLogger.onChild = (child) => {
|
|||
var _Auth = class extends import_events.EventEmitter {
|
||||
constructor(options) {
|
||||
super();
|
||||
// 3 days
|
||||
this.logger = rootLogger.child({ component: "Auth" });
|
||||
this.dataStore = null;
|
||||
this.pollingTokenTimer = null;
|
||||
this.stopPollingTokenTimer = null;
|
||||
this.refreshTokenTimer = null;
|
||||
this.authApi = null;
|
||||
this.jwt = null;
|
||||
|
|
@ -644,10 +656,16 @@ var _Auth = class extends import_events.EventEmitter {
|
|||
const storedJwt = this.dataStore.data["auth"]?.[this.endpoint]?.jwt;
|
||||
if (typeof storedJwt === "string" && this.jwt?.token !== storedJwt) {
|
||||
this.logger.debug({ storedJwt }, "Load jwt from data store.");
|
||||
this.jwt = {
|
||||
const jwt = {
|
||||
token: storedJwt,
|
||||
payload: (0, import_jwt_decode.default)(storedJwt)
|
||||
};
|
||||
if (jwt.payload.exp * 1e3 - Date.now() < _Auth.tokenStrategy.refresh.beforeExpire) {
|
||||
this.jwt = await this.refreshToken(jwt);
|
||||
await this.save();
|
||||
} else {
|
||||
this.jwt = jwt;
|
||||
}
|
||||
this.scheduleRefreshToken();
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -686,10 +704,15 @@ var _Auth = class extends import_events.EventEmitter {
|
|||
clearInterval(this.pollingTokenTimer);
|
||||
this.pollingTokenTimer = null;
|
||||
}
|
||||
if (this.stopPollingTokenTimer) {
|
||||
clearTimeout(this.stopPollingTokenTimer);
|
||||
this.stopPollingTokenTimer = null;
|
||||
}
|
||||
}
|
||||
async requestToken() {
|
||||
try {
|
||||
await this.reset();
|
||||
this.logger.debug("Start to request device token");
|
||||
const deviceToken = await this.authApi.api.deviceToken({ auth_url: this.endpoint });
|
||||
this.logger.debug({ deviceToken }, "Request device token response");
|
||||
const authUrl = new URL(_Auth.authPageUrl);
|
||||
|
|
@ -701,6 +724,28 @@ var _Auth = class extends import_events.EventEmitter {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
async refreshToken(jwt, retry = 0) {
|
||||
try {
|
||||
this.logger.debug({ retry }, "Start to refresh token");
|
||||
const refreshedJwt = await this.authApi.api.deviceTokenRefresh(jwt.token);
|
||||
this.logger.debug({ refreshedJwt }, "Refresh token response");
|
||||
return {
|
||||
token: refreshedJwt.data.jwt,
|
||||
payload: (0, import_jwt_decode.default)(refreshedJwt.data.jwt)
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && [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) {
|
||||
this.logger.debug("Retry refreshing jwt");
|
||||
return this.refreshToken(jwt, retry + 1);
|
||||
}
|
||||
}
|
||||
throw { ...error, retry };
|
||||
}
|
||||
}
|
||||
async schedulePollingToken(code) {
|
||||
this.pollingTokenTimer = setInterval(async () => {
|
||||
try {
|
||||
|
|
@ -722,7 +767,13 @@ var _Auth = class extends import_events.EventEmitter {
|
|||
this.logger.error({ error }, "Error when polling jwt");
|
||||
}
|
||||
}
|
||||
}, _Auth.pollTokenInterval);
|
||||
}, _Auth.tokenStrategy.polling.interval);
|
||||
this.stopPollingTokenTimer = setTimeout(() => {
|
||||
if (this.pollingTokenTimer) {
|
||||
clearInterval(this.pollingTokenTimer);
|
||||
this.pollingTokenTimer = null;
|
||||
}
|
||||
}, _Auth.tokenStrategy.polling.timeout);
|
||||
}
|
||||
scheduleRefreshToken() {
|
||||
if (this.refreshTokenTimer) {
|
||||
|
|
@ -732,17 +783,39 @@ var _Auth = class extends import_events.EventEmitter {
|
|||
if (!this.jwt) {
|
||||
return null;
|
||||
}
|
||||
const refreshDelay = Math.max(0, this.jwt.payload.exp * 1e3 - Date.now() - _Auth.refreshTokenInterval);
|
||||
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.logger.debug({ expireAt: this.jwt.payload.exp }, "Refresh token");
|
||||
this.jwt = await this.refreshToken(this.jwt);
|
||||
await this.save();
|
||||
this.scheduleRefreshToken();
|
||||
super.emit("updated", this.jwt);
|
||||
}, refreshDelay);
|
||||
}
|
||||
};
|
||||
var Auth = _Auth;
|
||||
Auth.authPageUrl = "https://app.tabbyml.com/account/device-token";
|
||||
Auth.pollTokenInterval = 5e3;
|
||||
// 5 seconds
|
||||
Auth.refreshTokenInterval = 1e3 * 60 * 60 * 24 * 3;
|
||||
Auth.tokenStrategy = {
|
||||
polling: {
|
||||
// polling token after auth url generated
|
||||
interval: 5e3,
|
||||
// polling token every 5 seconds
|
||||
timeout: 5 * 60 * 1e3
|
||||
// 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
|
||||
beforeExpire: 30 * 60 * 1e3,
|
||||
maxTry: 5,
|
||||
// try to refresh token 5 times
|
||||
retryDelay: 2e3
|
||||
// retry after 2 seconds
|
||||
}
|
||||
};
|
||||
|
||||
// src/AgentConfig.ts
|
||||
var defaultAgentConfig = {
|
||||
|
|
@ -1047,7 +1120,6 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
|
|||
const agent = new _TabbyAgent();
|
||||
agent.dataStore = options?.dataStore;
|
||||
agent.anonymousUsageLogger = await AnonymousUsageLogger.create({ dataStore: options?.dataStore });
|
||||
await agent.applyConfig();
|
||||
return agent;
|
||||
}
|
||||
async applyConfig() {
|
||||
|
|
@ -1119,9 +1191,7 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
|
|||
if (options.client) {
|
||||
allLoggers.forEach((logger2) => logger2.setBindings && logger2.setBindings({ client: options.client }));
|
||||
}
|
||||
if (options.config) {
|
||||
await this.updateConfig(options.config);
|
||||
}
|
||||
await this.updateConfig(options.config || {});
|
||||
await this.anonymousUsageLogger.event("AgentInitialized", {
|
||||
client: options.client
|
||||
});
|
||||
|
|
@ -1138,7 +1208,7 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
|
|||
super.emit("configUpdated", event);
|
||||
}
|
||||
await this.healthCheck();
|
||||
return this.status !== "notInitialized";
|
||||
return true;
|
||||
}
|
||||
getConfig() {
|
||||
return this.config;
|
||||
|
|
@ -1147,6 +1217,9 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
|
|||
return this.status;
|
||||
}
|
||||
startAuth() {
|
||||
if (this.status === "notInitialized") {
|
||||
throw new Error("Agent is not initialized");
|
||||
}
|
||||
return cancelable(
|
||||
this.healthCheck().then(() => {
|
||||
if (this.status === "unauthorized") {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -13261,6 +13261,18 @@ var ApiService = class {
|
|||
query
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param token
|
||||
* @returns DeviceTokenRefreshResponse Success
|
||||
* @throws ApiError
|
||||
*/
|
||||
deviceTokenRefresh(token) {
|
||||
return this.httpRequest.request({
|
||||
method: "POST",
|
||||
url: "/device-token/refresh",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param body object for anonymous usage tracking
|
||||
*/
|
||||
|
|
@ -13277,7 +13289,7 @@ var ApiService = class {
|
|||
var CloudApi = class {
|
||||
constructor(config2, HttpRequest = AxiosHttpRequest) {
|
||||
this.request = new HttpRequest({
|
||||
BASE: config2?.BASE ?? "https://tabbyml.app.tabbyml.com/tabby",
|
||||
BASE: config2?.BASE,
|
||||
VERSION: config2?.VERSION ?? "0.0.0",
|
||||
WITH_CREDENTIALS: config2?.WITH_CREDENTIALS ?? false,
|
||||
CREDENTIALS: config2?.CREDENTIALS ?? "include",
|
||||
|
|
@ -13339,10 +13351,10 @@ rootLogger.onChild = (child) => {
|
|||
var _Auth = class extends EventEmitter {
|
||||
constructor(options) {
|
||||
super();
|
||||
// 3 days
|
||||
this.logger = rootLogger.child({ component: "Auth" });
|
||||
this.dataStore = null;
|
||||
this.pollingTokenTimer = null;
|
||||
this.stopPollingTokenTimer = null;
|
||||
this.refreshTokenTimer = null;
|
||||
this.authApi = null;
|
||||
this.jwt = null;
|
||||
|
|
@ -13370,10 +13382,16 @@ var _Auth = class extends EventEmitter {
|
|||
const storedJwt = this.dataStore.data["auth"]?.[this.endpoint]?.jwt;
|
||||
if (typeof storedJwt === "string" && this.jwt?.token !== storedJwt) {
|
||||
this.logger.debug({ storedJwt }, "Load jwt from data store.");
|
||||
this.jwt = {
|
||||
const jwt = {
|
||||
token: storedJwt,
|
||||
payload: jwt_decode_esm_default(storedJwt)
|
||||
};
|
||||
if (jwt.payload.exp * 1e3 - Date.now() < _Auth.tokenStrategy.refresh.beforeExpire) {
|
||||
this.jwt = await this.refreshToken(jwt);
|
||||
await this.save();
|
||||
} else {
|
||||
this.jwt = jwt;
|
||||
}
|
||||
this.scheduleRefreshToken();
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -13412,10 +13430,15 @@ var _Auth = class extends EventEmitter {
|
|||
clearInterval(this.pollingTokenTimer);
|
||||
this.pollingTokenTimer = null;
|
||||
}
|
||||
if (this.stopPollingTokenTimer) {
|
||||
clearTimeout(this.stopPollingTokenTimer);
|
||||
this.stopPollingTokenTimer = null;
|
||||
}
|
||||
}
|
||||
async requestToken() {
|
||||
try {
|
||||
await this.reset();
|
||||
this.logger.debug("Start to request device token");
|
||||
const deviceToken = await this.authApi.api.deviceToken({ auth_url: this.endpoint });
|
||||
this.logger.debug({ deviceToken }, "Request device token response");
|
||||
const authUrl = new URL(_Auth.authPageUrl);
|
||||
|
|
@ -13427,6 +13450,28 @@ var _Auth = class extends EventEmitter {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
async refreshToken(jwt, retry = 0) {
|
||||
try {
|
||||
this.logger.debug({ retry }, "Start to refresh token");
|
||||
const refreshedJwt = await this.authApi.api.deviceTokenRefresh(jwt.token);
|
||||
this.logger.debug({ refreshedJwt }, "Refresh token response");
|
||||
return {
|
||||
token: refreshedJwt.data.jwt,
|
||||
payload: jwt_decode_esm_default(refreshedJwt.data.jwt)
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && [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) {
|
||||
this.logger.debug("Retry refreshing jwt");
|
||||
return this.refreshToken(jwt, retry + 1);
|
||||
}
|
||||
}
|
||||
throw { ...error, retry };
|
||||
}
|
||||
}
|
||||
async schedulePollingToken(code) {
|
||||
this.pollingTokenTimer = setInterval(async () => {
|
||||
try {
|
||||
|
|
@ -13448,7 +13493,13 @@ var _Auth = class extends EventEmitter {
|
|||
this.logger.error({ error }, "Error when polling jwt");
|
||||
}
|
||||
}
|
||||
}, _Auth.pollTokenInterval);
|
||||
}, _Auth.tokenStrategy.polling.interval);
|
||||
this.stopPollingTokenTimer = setTimeout(() => {
|
||||
if (this.pollingTokenTimer) {
|
||||
clearInterval(this.pollingTokenTimer);
|
||||
this.pollingTokenTimer = null;
|
||||
}
|
||||
}, _Auth.tokenStrategy.polling.timeout);
|
||||
}
|
||||
scheduleRefreshToken() {
|
||||
if (this.refreshTokenTimer) {
|
||||
|
|
@ -13458,17 +13509,39 @@ var _Auth = class extends EventEmitter {
|
|||
if (!this.jwt) {
|
||||
return null;
|
||||
}
|
||||
const refreshDelay = Math.max(0, this.jwt.payload.exp * 1e3 - Date.now() - _Auth.refreshTokenInterval);
|
||||
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.logger.debug({ expireAt: this.jwt.payload.exp }, "Refresh token");
|
||||
this.jwt = await this.refreshToken(this.jwt);
|
||||
await this.save();
|
||||
this.scheduleRefreshToken();
|
||||
super.emit("updated", this.jwt);
|
||||
}, refreshDelay);
|
||||
}
|
||||
};
|
||||
var Auth = _Auth;
|
||||
Auth.authPageUrl = "https://app.tabbyml.com/account/device-token";
|
||||
Auth.pollTokenInterval = 5e3;
|
||||
// 5 seconds
|
||||
Auth.refreshTokenInterval = 1e3 * 60 * 60 * 24 * 3;
|
||||
Auth.tokenStrategy = {
|
||||
polling: {
|
||||
// polling token after auth url generated
|
||||
interval: 5e3,
|
||||
// polling token every 5 seconds
|
||||
timeout: 5 * 60 * 1e3
|
||||
// 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
|
||||
beforeExpire: 30 * 60 * 1e3,
|
||||
maxTry: 5,
|
||||
// try to refresh token 5 times
|
||||
retryDelay: 2e3
|
||||
// retry after 2 seconds
|
||||
}
|
||||
};
|
||||
|
||||
// src/AgentConfig.ts
|
||||
init_global();
|
||||
|
|
@ -15127,7 +15200,6 @@ var _TabbyAgent = class extends EventEmitter {
|
|||
const agent = new _TabbyAgent();
|
||||
agent.dataStore = options?.dataStore;
|
||||
agent.anonymousUsageLogger = await AnonymousUsageLogger.create({ dataStore: options?.dataStore });
|
||||
await agent.applyConfig();
|
||||
return agent;
|
||||
}
|
||||
async applyConfig() {
|
||||
|
|
@ -15199,9 +15271,7 @@ var _TabbyAgent = class extends EventEmitter {
|
|||
if (options.client) {
|
||||
allLoggers.forEach((logger2) => logger2.setBindings && logger2.setBindings({ client: options.client }));
|
||||
}
|
||||
if (options.config) {
|
||||
await this.updateConfig(options.config);
|
||||
}
|
||||
await this.updateConfig(options.config || {});
|
||||
await this.anonymousUsageLogger.event("AgentInitialized", {
|
||||
client: options.client
|
||||
});
|
||||
|
|
@ -15218,7 +15288,7 @@ var _TabbyAgent = class extends EventEmitter {
|
|||
super.emit("configUpdated", event);
|
||||
}
|
||||
await this.healthCheck();
|
||||
return this.status !== "notInitialized";
|
||||
return true;
|
||||
}
|
||||
getConfig() {
|
||||
return this.config;
|
||||
|
|
@ -15227,6 +15297,9 @@ var _TabbyAgent = class extends EventEmitter {
|
|||
return this.status;
|
||||
}
|
||||
startAuth() {
|
||||
if (this.status === "notInitialized") {
|
||||
throw new Error("Agent is not initialized");
|
||||
}
|
||||
return cancelable(
|
||||
this.healthCheck().then(() => {
|
||||
if (this.status === "unauthorized") {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -9,18 +9,33 @@ export type StorageData = {
|
|||
auth: { [endpoint: string]: { jwt: string } };
|
||||
};
|
||||
|
||||
type JWT = { token: string; payload: { email: string; exp: number } };
|
||||
|
||||
export class Auth extends EventEmitter {
|
||||
static readonly authPageUrl = "https://app.tabbyml.com/account/device-token";
|
||||
static readonly pollTokenInterval = 5000; // 5 seconds
|
||||
static readonly refreshTokenInterval = 1000 * 60 * 60 * 24 * 3; // 3 days
|
||||
static readonly tokenStrategy = {
|
||||
polling: {
|
||||
// polling token after auth url generated
|
||||
interval: 5000, // polling token every 5 seconds
|
||||
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
|
||||
beforeExpire: 30 * 60 * 1000,
|
||||
maxTry: 5, // try to refresh token 5 times
|
||||
retryDelay: 2000, // retry after 2 seconds
|
||||
},
|
||||
};
|
||||
|
||||
private readonly logger = rootLogger.child({ component: "Auth" });
|
||||
readonly endpoint: string;
|
||||
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 authApi: CloudApi | null = null;
|
||||
private jwt: { token: string; payload: { email: string; exp: number } } | null = null;
|
||||
private jwt: JWT | null = null;
|
||||
|
||||
static async create(options: { endpoint: string; dataStore?: DataStore }): Promise<Auth> {
|
||||
const auth = new Auth(options);
|
||||
|
|
@ -54,10 +69,17 @@ export class Auth extends EventEmitter {
|
|||
const storedJwt = this.dataStore.data["auth"]?.[this.endpoint]?.jwt;
|
||||
if (typeof storedJwt === "string" && this.jwt?.token !== storedJwt) {
|
||||
this.logger.debug({ storedJwt }, "Load jwt from data store.");
|
||||
this.jwt = {
|
||||
const jwt: JWT = {
|
||||
token: storedJwt,
|
||||
payload: decodeJwt(storedJwt),
|
||||
};
|
||||
// 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);
|
||||
await this.save();
|
||||
} else {
|
||||
this.jwt = jwt;
|
||||
}
|
||||
this.scheduleRefreshToken();
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
|
@ -95,11 +117,16 @@ export class Auth extends EventEmitter {
|
|||
clearInterval(this.pollingTokenTimer);
|
||||
this.pollingTokenTimer = null;
|
||||
}
|
||||
if (this.stopPollingTokenTimer) {
|
||||
clearTimeout(this.stopPollingTokenTimer);
|
||||
this.stopPollingTokenTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
async requestToken(): Promise<string> {
|
||||
try {
|
||||
await this.reset();
|
||||
this.logger.debug("Start to request device token");
|
||||
const deviceToken = await this.authApi.api.deviceToken({ auth_url: this.endpoint });
|
||||
this.logger.debug({ deviceToken }, "Request device token response");
|
||||
const authUrl = new URL(Auth.authPageUrl);
|
||||
|
|
@ -112,7 +139,31 @@ export class Auth extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
async schedulePollingToken(code: string) {
|
||||
private async refreshToken(jwt: JWT, retry = 0): Promise<JWT> {
|
||||
try {
|
||||
this.logger.debug({ retry }, "Start to refresh token");
|
||||
const refreshedJwt = await this.authApi.api.deviceTokenRefresh(jwt.token);
|
||||
this.logger.debug({ refreshedJwt }, "Refresh token response");
|
||||
return {
|
||||
token: refreshedJwt.data.jwt,
|
||||
payload: decodeJwt(refreshedJwt.data.jwt),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && [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) {
|
||||
this.logger.debug("Retry refreshing jwt");
|
||||
return this.refreshToken(jwt, retry + 1);
|
||||
}
|
||||
}
|
||||
throw { ...error, retry };
|
||||
}
|
||||
}
|
||||
|
||||
private async schedulePollingToken(code: string) {
|
||||
this.pollingTokenTimer = setInterval(async () => {
|
||||
try {
|
||||
const response = await this.authApi.api.deviceTokenAccept({ code });
|
||||
|
|
@ -134,7 +185,13 @@ export class Auth extends EventEmitter {
|
|||
this.logger.error({ error }, "Error when polling jwt");
|
||||
}
|
||||
}
|
||||
}, Auth.pollTokenInterval);
|
||||
}, Auth.tokenStrategy.polling.interval);
|
||||
this.stopPollingTokenTimer = setTimeout(() => {
|
||||
if (this.pollingTokenTimer) {
|
||||
clearInterval(this.pollingTokenTimer);
|
||||
this.pollingTokenTimer = null;
|
||||
}
|
||||
}, Auth.tokenStrategy.polling.timeout);
|
||||
}
|
||||
|
||||
private scheduleRefreshToken() {
|
||||
|
|
@ -146,10 +203,16 @@ export class Auth extends EventEmitter {
|
|||
return null;
|
||||
}
|
||||
|
||||
const refreshDelay = Math.max(0, this.jwt.payload.exp * 1000 - Date.now() - Auth.refreshTokenInterval);
|
||||
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.logger.debug({ expireAt: this.jwt.payload.exp }, "Refresh token");
|
||||
// FIXME: not implemented
|
||||
this.jwt = await this.refreshToken(this.jwt);
|
||||
await this.save();
|
||||
this.scheduleRefreshToken();
|
||||
super.emit("updated", this.jwt);
|
||||
}, refreshDelay);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ export class TabbyAgent extends EventEmitter implements Agent {
|
|||
const agent = new TabbyAgent();
|
||||
agent.dataStore = options?.dataStore;
|
||||
agent.anonymousUsageLogger = await AnonymousUsageLogger.create({ dataStore: options?.dataStore });
|
||||
await agent.applyConfig();
|
||||
return agent;
|
||||
}
|
||||
|
||||
|
|
@ -142,9 +141,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
|
|||
// `pino.Logger.setBindings` is not present in the browser
|
||||
allLoggers.forEach((logger) => logger.setBindings && logger.setBindings({ client: options.client }));
|
||||
}
|
||||
if (options.config) {
|
||||
await this.updateConfig(options.config);
|
||||
}
|
||||
await this.updateConfig(options.config || {});
|
||||
await this.anonymousUsageLogger.event("AgentInitialized", {
|
||||
client: options.client,
|
||||
});
|
||||
|
|
@ -162,7 +159,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
|
|||
super.emit("configUpdated", event);
|
||||
}
|
||||
await this.healthCheck();
|
||||
return this.status !== "notInitialized";
|
||||
return true;
|
||||
}
|
||||
|
||||
public getConfig(): AgentConfig {
|
||||
|
|
@ -174,6 +171,9 @@ export class TabbyAgent extends EventEmitter implements Agent {
|
|||
}
|
||||
|
||||
public startAuth(): CancelablePromise<string | null> {
|
||||
if (this.status === "notInitialized") {
|
||||
throw new Error("Agent is not initialized");
|
||||
}
|
||||
return cancelable(
|
||||
this.healthCheck().then(() => {
|
||||
if (this.status === "unauthorized") {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export class CloudApi {
|
|||
|
||||
constructor(config?: Partial<OpenAPIConfig>, HttpRequest: HttpRequestConstructor = AxiosHttpRequest) {
|
||||
this.request = new HttpRequest({
|
||||
BASE: config?.BASE ?? 'https://tabbyml.app.tabbyml.com/tabby',
|
||||
BASE: config?.BASE,
|
||||
VERSION: config?.VERSION ?? "0.0.0",
|
||||
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
|
||||
CREDENTIALS: config?.CREDENTIALS ?? "include",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
export type DeviceTokenRefreshResponse = {
|
||||
data: {
|
||||
jwt: string;
|
||||
};
|
||||
};
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
export type DeviceTokenResponse = {
|
||||
export type DeviceTokenRequest = {
|
||||
auth_url: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import type { CancelablePromise } from "../../generated/core/CancelablePromise";
|
||||
import type { BaseHttpRequest } from "../../generated/core/BaseHttpRequest";
|
||||
|
||||
import { DeviceTokenRequest } from "../models/DeviceTokenRequest";
|
||||
import { DeviceTokenResponse } from "../models/DeviceTokenResponse";
|
||||
import { DeviceTokenAcceptResponse } from "../models/DeviceTokenAcceptResponse";
|
||||
import type { DeviceTokenRequest } from "../models/DeviceTokenRequest";
|
||||
import type { DeviceTokenResponse } from "../models/DeviceTokenResponse";
|
||||
import type { DeviceTokenAcceptResponse } from "../models/DeviceTokenAcceptResponse";
|
||||
import type { DeviceTokenRefreshResponse } from "../models/DeviceTokenRefreshResponse";
|
||||
|
||||
export class ApiService {
|
||||
constructor(public readonly httpRequest: BaseHttpRequest) {}
|
||||
|
|
@ -33,6 +34,19 @@ export class ApiService {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param token
|
||||
* @returns DeviceTokenRefreshResponse Success
|
||||
* @throws ApiError
|
||||
*/
|
||||
public deviceTokenRefresh(token: string): CancelablePromise<DeviceTokenRefreshResponse> {
|
||||
return this.httpRequest.request({
|
||||
method: "POST",
|
||||
url: "/device-token/refresh",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param body object for anonymous usage tracking
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -140,16 +140,22 @@ const emitEvent: Command = {
|
|||
|
||||
const openAuthPage: Command = {
|
||||
command: "tabby.openAuthPage",
|
||||
callback: () => {
|
||||
callback: (callbacks?: { onOpenAuthPage?: () => void }) => {
|
||||
agent()
|
||||
.startAuth()
|
||||
.then((authUrl) => {
|
||||
if (authUrl) {
|
||||
callbacks?.onOpenAuthPage?.();
|
||||
env.openExternal(Uri.parse(authUrl));
|
||||
} else if (agent().getStatus() === "ready") {
|
||||
notifications.showInformationWhenStartAuthButAlreadyAuthorized();
|
||||
} else {
|
||||
notifications.showInformationWhenStartAuthFailed();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.debug("Error to start auth", { error })
|
||||
console.debug("Error to start auth", { error });
|
||||
notifications.showInformationWhenStartAuthFailed();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,17 +52,17 @@ function showInformationWhenDisconnected() {
|
|||
});
|
||||
}
|
||||
|
||||
function showInformationStartAuth() {
|
||||
function showInformationStartAuth(callbacks?: { onOpenAuthPage?: () => void }) {
|
||||
window
|
||||
.showInformationMessage(
|
||||
"Tabby Server requires authentication. Continue to open authentication page in your browser.",
|
||||
"Tabby Server requires authorization. Continue to open authorization page in your browser.",
|
||||
"Continue",
|
||||
"Settings"
|
||||
)
|
||||
.then((selection) => {
|
||||
switch (selection) {
|
||||
case "Continue":
|
||||
commands.executeCommand("tabby.openAuthPage");
|
||||
commands.executeCommand("tabby.openAuthPage", callbacks);
|
||||
break;
|
||||
case "Settings":
|
||||
commands.executeCommand("tabby.openSettings");
|
||||
|
|
@ -74,6 +74,20 @@ function showInformationAuthSuccess() {
|
|||
window.showInformationMessage("Congrats, you're authorized, start to use Tabby now.");
|
||||
}
|
||||
|
||||
function showInformationWhenStartAuthButAlreadyAuthorized() {
|
||||
window.showInformationMessage("You are already authorized now.");
|
||||
}
|
||||
|
||||
function showInformationWhenStartAuthFailed() {
|
||||
window.showInformationMessage("Cannot connect to server. Please check settings.", "Settings").then((selection) => {
|
||||
switch (selection) {
|
||||
case "Settings":
|
||||
commands.executeCommand("tabby.openSettings");
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const notifications = {
|
||||
showInformationWhenLoading,
|
||||
showInformationWhenDisabled,
|
||||
|
|
@ -81,4 +95,6 @@ export const notifications = {
|
|||
showInformationWhenDisconnected,
|
||||
showInformationStartAuth,
|
||||
showInformationAuthSuccess,
|
||||
showInformationWhenStartAuthButAlreadyAuthorized,
|
||||
showInformationWhenStartAuthFailed,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,11 +32,23 @@ const fsm = createMachine({
|
|||
entry: () => toDisconnected(),
|
||||
},
|
||||
unauthorized: {
|
||||
on: { ready: "ready", disconnected: "disconnected", disabled: "disabled" },
|
||||
on: {
|
||||
ready: "ready",
|
||||
disconnected: "disconnected",
|
||||
disabled: "disabled",
|
||||
openAuthPage: "unauthorizedAndAuthPageOpen",
|
||||
},
|
||||
entry: () => {
|
||||
toUnauthorized();
|
||||
notifications.showInformationStartAuth();
|
||||
notifications.showInformationStartAuth({
|
||||
onOpenAuthPage: () => {
|
||||
fsmService.send("openAuthPage");
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
unauthorizedAndAuthPageOpen: {
|
||||
on: { ready: "ready", disconnected: "disconnected", disabled: "disabled" },
|
||||
exit: (_, event) => {
|
||||
if (event.type === "ready") {
|
||||
notifications.showInformationAuthSuccess();
|
||||
|
|
@ -79,7 +91,7 @@ function toUnauthorized() {
|
|||
item.color = colorWarning;
|
||||
item.backgroundColor = backgroundColorWarning;
|
||||
item.text = `${iconUnauthorized} ${label}`;
|
||||
item.tooltip = "Tabby Server requires authentication. Click to continue.";
|
||||
item.tooltip = "Tabby Server requires authorization. Click to continue.";
|
||||
item.command = { title: "", command: "tabby.statusBarItemClicked", arguments: ["unauthorized"] };
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue