feat: agent auth support to refresh token. (#262)

sweep/improve-logging-information
Zhiming Ma 2023-06-23 14:43:55 +08:00 committed by GitHub
parent b6845ddac0
commit 1752f1555b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 388 additions and 126 deletions

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

View File

@ -543,6 +543,18 @@ var ApiService = class {
query 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 * @param body object for anonymous usage tracking
*/ */
@ -559,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 ?? "https://tabbyml.app.tabbyml.com/tabby", BASE: config?.BASE,
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",
@ -613,10 +625,10 @@ rootLogger.onChild = (child) => {
var _Auth = class extends import_events.EventEmitter { var _Auth = class extends import_events.EventEmitter {
constructor(options) { constructor(options) {
super(); super();
// 3 days
this.logger = rootLogger.child({ component: "Auth" }); this.logger = rootLogger.child({ component: "Auth" });
this.dataStore = null; this.dataStore = null;
this.pollingTokenTimer = null; this.pollingTokenTimer = null;
this.stopPollingTokenTimer = null;
this.refreshTokenTimer = null; this.refreshTokenTimer = null;
this.authApi = null; this.authApi = null;
this.jwt = null; this.jwt = null;
@ -644,10 +656,16 @@ var _Auth = class extends import_events.EventEmitter {
const storedJwt = this.dataStore.data["auth"]?.[this.endpoint]?.jwt; const storedJwt = this.dataStore.data["auth"]?.[this.endpoint]?.jwt;
if (typeof storedJwt === "string" && this.jwt?.token !== storedJwt) { if (typeof storedJwt === "string" && this.jwt?.token !== storedJwt) {
this.logger.debug({ storedJwt }, "Load jwt from data store."); this.logger.debug({ storedJwt }, "Load jwt from data store.");
this.jwt = { const jwt = {
token: storedJwt, token: storedJwt,
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) {
this.jwt = await this.refreshToken(jwt);
await this.save();
} else {
this.jwt = jwt;
}
this.scheduleRefreshToken(); this.scheduleRefreshToken();
} }
} catch (error) { } catch (error) {
@ -686,10 +704,15 @@ var _Auth = class extends import_events.EventEmitter {
clearInterval(this.pollingTokenTimer); clearInterval(this.pollingTokenTimer);
this.pollingTokenTimer = null; this.pollingTokenTimer = null;
} }
if (this.stopPollingTokenTimer) {
clearTimeout(this.stopPollingTokenTimer);
this.stopPollingTokenTimer = null;
}
} }
async requestToken() { async requestToken() {
try { try {
await this.reset(); await this.reset();
this.logger.debug("Start to request device token");
const deviceToken = await this.authApi.api.deviceToken({ auth_url: this.endpoint }); const deviceToken = await this.authApi.api.deviceToken({ auth_url: this.endpoint });
this.logger.debug({ deviceToken }, "Request device token response"); this.logger.debug({ deviceToken }, "Request device token response");
const authUrl = new URL(_Auth.authPageUrl); const authUrl = new URL(_Auth.authPageUrl);
@ -701,6 +724,28 @@ var _Auth = class extends import_events.EventEmitter {
throw error; 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) { async schedulePollingToken(code) {
this.pollingTokenTimer = setInterval(async () => { this.pollingTokenTimer = setInterval(async () => {
try { try {
@ -722,7 +767,13 @@ var _Auth = class extends import_events.EventEmitter {
this.logger.error({ error }, "Error when polling jwt"); 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() { scheduleRefreshToken() {
if (this.refreshTokenTimer) { if (this.refreshTokenTimer) {
@ -732,17 +783,39 @@ var _Auth = class extends import_events.EventEmitter {
if (!this.jwt) { if (!this.jwt) {
return null; 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.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); }, refreshDelay);
} }
}; };
var Auth = _Auth; var Auth = _Auth;
Auth.authPageUrl = "https://app.tabbyml.com/account/device-token"; Auth.authPageUrl = "https://app.tabbyml.com/account/device-token";
Auth.pollTokenInterval = 5e3; Auth.tokenStrategy = {
// 5 seconds polling: {
Auth.refreshTokenInterval = 1e3 * 60 * 60 * 24 * 3; // 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 // src/AgentConfig.ts
var defaultAgentConfig = { var defaultAgentConfig = {
@ -1047,7 +1120,6 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
const agent = new _TabbyAgent(); const agent = new _TabbyAgent();
agent.dataStore = options?.dataStore; agent.dataStore = options?.dataStore;
agent.anonymousUsageLogger = await AnonymousUsageLogger.create({ dataStore: options?.dataStore }); agent.anonymousUsageLogger = await AnonymousUsageLogger.create({ dataStore: options?.dataStore });
await agent.applyConfig();
return agent; return agent;
} }
async applyConfig() { async applyConfig() {
@ -1119,9 +1191,7 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
if (options.client) { if (options.client) {
allLoggers.forEach((logger2) => logger2.setBindings && logger2.setBindings({ client: 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", { await this.anonymousUsageLogger.event("AgentInitialized", {
client: options.client client: options.client
}); });
@ -1138,7 +1208,7 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
super.emit("configUpdated", event); super.emit("configUpdated", event);
} }
await this.healthCheck(); await this.healthCheck();
return this.status !== "notInitialized"; return true;
} }
getConfig() { getConfig() {
return this.config; return this.config;
@ -1147,6 +1217,9 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
return this.status; return this.status;
} }
startAuth() { startAuth() {
if (this.status === "notInitialized") {
throw new Error("Agent is not initialized");
}
return cancelable( return cancelable(
this.healthCheck().then(() => { this.healthCheck().then(() => {
if (this.status === "unauthorized") { if (this.status === "unauthorized") {

File diff suppressed because one or more lines are too long

View File

@ -13261,6 +13261,18 @@ var ApiService = class {
query 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 * @param body object for anonymous usage tracking
*/ */
@ -13277,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 ?? "https://tabbyml.app.tabbyml.com/tabby", BASE: config2?.BASE,
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",
@ -13339,10 +13351,10 @@ rootLogger.onChild = (child) => {
var _Auth = class extends EventEmitter { var _Auth = class extends EventEmitter {
constructor(options) { constructor(options) {
super(); super();
// 3 days
this.logger = rootLogger.child({ component: "Auth" }); this.logger = rootLogger.child({ component: "Auth" });
this.dataStore = null; this.dataStore = null;
this.pollingTokenTimer = null; this.pollingTokenTimer = null;
this.stopPollingTokenTimer = null;
this.refreshTokenTimer = null; this.refreshTokenTimer = null;
this.authApi = null; this.authApi = null;
this.jwt = null; this.jwt = null;
@ -13370,10 +13382,16 @@ var _Auth = class extends EventEmitter {
const storedJwt = this.dataStore.data["auth"]?.[this.endpoint]?.jwt; const storedJwt = this.dataStore.data["auth"]?.[this.endpoint]?.jwt;
if (typeof storedJwt === "string" && this.jwt?.token !== storedJwt) { if (typeof storedJwt === "string" && this.jwt?.token !== storedJwt) {
this.logger.debug({ storedJwt }, "Load jwt from data store."); this.logger.debug({ storedJwt }, "Load jwt from data store.");
this.jwt = { const jwt = {
token: storedJwt, token: storedJwt,
payload: jwt_decode_esm_default(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(); this.scheduleRefreshToken();
} }
} catch (error) { } catch (error) {
@ -13412,10 +13430,15 @@ var _Auth = class extends EventEmitter {
clearInterval(this.pollingTokenTimer); clearInterval(this.pollingTokenTimer);
this.pollingTokenTimer = null; this.pollingTokenTimer = null;
} }
if (this.stopPollingTokenTimer) {
clearTimeout(this.stopPollingTokenTimer);
this.stopPollingTokenTimer = null;
}
} }
async requestToken() { async requestToken() {
try { try {
await this.reset(); await this.reset();
this.logger.debug("Start to request device token");
const deviceToken = await this.authApi.api.deviceToken({ auth_url: this.endpoint }); const deviceToken = await this.authApi.api.deviceToken({ auth_url: this.endpoint });
this.logger.debug({ deviceToken }, "Request device token response"); this.logger.debug({ deviceToken }, "Request device token response");
const authUrl = new URL(_Auth.authPageUrl); const authUrl = new URL(_Auth.authPageUrl);
@ -13427,6 +13450,28 @@ var _Auth = class extends EventEmitter {
throw error; 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) { async schedulePollingToken(code) {
this.pollingTokenTimer = setInterval(async () => { this.pollingTokenTimer = setInterval(async () => {
try { try {
@ -13448,7 +13493,13 @@ var _Auth = class extends EventEmitter {
this.logger.error({ error }, "Error when polling jwt"); 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() { scheduleRefreshToken() {
if (this.refreshTokenTimer) { if (this.refreshTokenTimer) {
@ -13458,17 +13509,39 @@ var _Auth = class extends EventEmitter {
if (!this.jwt) { if (!this.jwt) {
return null; 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.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); }, refreshDelay);
} }
}; };
var Auth = _Auth; var Auth = _Auth;
Auth.authPageUrl = "https://app.tabbyml.com/account/device-token"; Auth.authPageUrl = "https://app.tabbyml.com/account/device-token";
Auth.pollTokenInterval = 5e3; Auth.tokenStrategy = {
// 5 seconds polling: {
Auth.refreshTokenInterval = 1e3 * 60 * 60 * 24 * 3; // 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 // src/AgentConfig.ts
init_global(); init_global();
@ -15127,7 +15200,6 @@ var _TabbyAgent = class extends EventEmitter {
const agent = new _TabbyAgent(); const agent = new _TabbyAgent();
agent.dataStore = options?.dataStore; agent.dataStore = options?.dataStore;
agent.anonymousUsageLogger = await AnonymousUsageLogger.create({ dataStore: options?.dataStore }); agent.anonymousUsageLogger = await AnonymousUsageLogger.create({ dataStore: options?.dataStore });
await agent.applyConfig();
return agent; return agent;
} }
async applyConfig() { async applyConfig() {
@ -15199,9 +15271,7 @@ var _TabbyAgent = class extends EventEmitter {
if (options.client) { if (options.client) {
allLoggers.forEach((logger2) => logger2.setBindings && logger2.setBindings({ client: 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", { await this.anonymousUsageLogger.event("AgentInitialized", {
client: options.client client: options.client
}); });
@ -15218,7 +15288,7 @@ var _TabbyAgent = class extends EventEmitter {
super.emit("configUpdated", event); super.emit("configUpdated", event);
} }
await this.healthCheck(); await this.healthCheck();
return this.status !== "notInitialized"; return true;
} }
getConfig() { getConfig() {
return this.config; return this.config;
@ -15227,6 +15297,9 @@ var _TabbyAgent = class extends EventEmitter {
return this.status; return this.status;
} }
startAuth() { startAuth() {
if (this.status === "notInitialized") {
throw new Error("Agent is not initialized");
}
return cancelable( return cancelable(
this.healthCheck().then(() => { this.healthCheck().then(() => {
if (this.status === "unauthorized") { if (this.status === "unauthorized") {

File diff suppressed because one or more lines are too long

View File

@ -9,18 +9,33 @@ export type StorageData = {
auth: { [endpoint: string]: { jwt: string } }; auth: { [endpoint: string]: { jwt: string } };
}; };
type JWT = { token: string; payload: { email: string; exp: number } };
export class Auth extends EventEmitter { export class Auth extends EventEmitter {
static readonly authPageUrl = "https://app.tabbyml.com/account/device-token"; static readonly authPageUrl = "https://app.tabbyml.com/account/device-token";
static readonly pollTokenInterval = 5000; // 5 seconds static readonly tokenStrategy = {
static readonly refreshTokenInterval = 1000 * 60 * 60 * 24 * 3; // 3 days 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" }); private readonly logger = rootLogger.child({ component: "Auth" });
readonly endpoint: string; readonly endpoint: string;
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 refreshTokenTimer: ReturnType<typeof setTimeout> | null = null; private refreshTokenTimer: ReturnType<typeof setTimeout> | null = null;
private authApi: CloudApi | 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> { static async create(options: { endpoint: string; dataStore?: DataStore }): Promise<Auth> {
const auth = new Auth(options); const auth = new Auth(options);
@ -54,10 +69,17 @@ export class Auth extends EventEmitter {
const storedJwt = this.dataStore.data["auth"]?.[this.endpoint]?.jwt; const storedJwt = this.dataStore.data["auth"]?.[this.endpoint]?.jwt;
if (typeof storedJwt === "string" && this.jwt?.token !== storedJwt) { if (typeof storedJwt === "string" && this.jwt?.token !== storedJwt) {
this.logger.debug({ storedJwt }, "Load jwt from data store."); this.logger.debug({ storedJwt }, "Load jwt from data store.");
this.jwt = { const jwt: JWT = {
token: storedJwt, token: storedJwt,
payload: decodeJwt(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(); this.scheduleRefreshToken();
} }
} catch (error: any) { } catch (error: any) {
@ -95,11 +117,16 @@ export class Auth extends EventEmitter {
clearInterval(this.pollingTokenTimer); clearInterval(this.pollingTokenTimer);
this.pollingTokenTimer = null; this.pollingTokenTimer = null;
} }
if (this.stopPollingTokenTimer) {
clearTimeout(this.stopPollingTokenTimer);
this.stopPollingTokenTimer = null;
}
} }
async requestToken(): Promise<string> { async requestToken(): Promise<string> {
try { try {
await this.reset(); await this.reset();
this.logger.debug("Start to request device token");
const deviceToken = await this.authApi.api.deviceToken({ auth_url: this.endpoint }); const deviceToken = await this.authApi.api.deviceToken({ auth_url: this.endpoint });
this.logger.debug({ deviceToken }, "Request device token response"); this.logger.debug({ deviceToken }, "Request device token response");
const authUrl = new URL(Auth.authPageUrl); 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 () => { this.pollingTokenTimer = setInterval(async () => {
try { try {
const response = await this.authApi.api.deviceTokenAccept({ code }); 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"); 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() { private scheduleRefreshToken() {
@ -146,10 +203,16 @@ export class Auth extends EventEmitter {
return null; 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.refreshTokenTimer = setTimeout(async () => {
this.logger.debug({ expireAt: this.jwt.payload.exp }, "Refresh token"); this.jwt = await this.refreshToken(this.jwt);
// FIXME: not implemented await this.save();
this.scheduleRefreshToken();
super.emit("updated", this.jwt);
}, refreshDelay); }, refreshDelay);
} }
} }

View File

@ -56,7 +56,6 @@ export class TabbyAgent extends EventEmitter implements Agent {
const agent = new TabbyAgent(); const agent = new TabbyAgent();
agent.dataStore = options?.dataStore; agent.dataStore = options?.dataStore;
agent.anonymousUsageLogger = await AnonymousUsageLogger.create({ dataStore: options?.dataStore }); agent.anonymousUsageLogger = await AnonymousUsageLogger.create({ dataStore: options?.dataStore });
await agent.applyConfig();
return agent; return agent;
} }
@ -142,9 +141,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
// `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 && logger.setBindings({ client: options.client }));
} }
if (options.config) { await this.updateConfig(options.config || {});
await this.updateConfig(options.config);
}
await this.anonymousUsageLogger.event("AgentInitialized", { await this.anonymousUsageLogger.event("AgentInitialized", {
client: options.client, client: options.client,
}); });
@ -162,7 +159,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
super.emit("configUpdated", event); super.emit("configUpdated", event);
} }
await this.healthCheck(); await this.healthCheck();
return this.status !== "notInitialized"; return true;
} }
public getConfig(): AgentConfig { public getConfig(): AgentConfig {
@ -174,6 +171,9 @@ export class TabbyAgent extends EventEmitter implements Agent {
} }
public startAuth(): CancelablePromise<string | null> { public startAuth(): CancelablePromise<string | null> {
if (this.status === "notInitialized") {
throw new Error("Agent is not initialized");
}
return cancelable( return cancelable(
this.healthCheck().then(() => { this.healthCheck().then(() => {
if (this.status === "unauthorized") { if (this.status === "unauthorized") {

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 ?? 'https://tabbyml.app.tabbyml.com/tabby', BASE: config?.BASE,
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",

View File

@ -0,0 +1,5 @@
export type DeviceTokenRefreshResponse = {
data: {
jwt: string;
};
};

View File

@ -1,3 +1,3 @@
export type DeviceTokenResponse = { export type DeviceTokenRequest = {
auth_url: string; auth_url: string;
}; };

View File

@ -1,9 +1,10 @@
import type { CancelablePromise } from "../../generated/core/CancelablePromise"; import type { CancelablePromise } from "../../generated/core/CancelablePromise";
import type { BaseHttpRequest } from "../../generated/core/BaseHttpRequest"; import type { BaseHttpRequest } from "../../generated/core/BaseHttpRequest";
import { DeviceTokenRequest } from "../models/DeviceTokenRequest"; import type { DeviceTokenRequest } from "../models/DeviceTokenRequest";
import { DeviceTokenResponse } from "../models/DeviceTokenResponse"; import type { DeviceTokenResponse } from "../models/DeviceTokenResponse";
import { DeviceTokenAcceptResponse } from "../models/DeviceTokenAcceptResponse"; import type { DeviceTokenAcceptResponse } from "../models/DeviceTokenAcceptResponse";
import type { DeviceTokenRefreshResponse } from "../models/DeviceTokenRefreshResponse";
export class ApiService { export class ApiService {
constructor(public readonly httpRequest: BaseHttpRequest) {} 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 * @param body object for anonymous usage tracking
*/ */

View File

@ -140,16 +140,22 @@ const emitEvent: Command = {
const openAuthPage: Command = { const openAuthPage: Command = {
command: "tabby.openAuthPage", command: "tabby.openAuthPage",
callback: () => { callback: (callbacks?: { onOpenAuthPage?: () => void }) => {
agent() agent()
.startAuth() .startAuth()
.then((authUrl) => { .then((authUrl) => {
if (authUrl) { if (authUrl) {
callbacks?.onOpenAuthPage?.();
env.openExternal(Uri.parse(authUrl)); env.openExternal(Uri.parse(authUrl));
} else if (agent().getStatus() === "ready") {
notifications.showInformationWhenStartAuthButAlreadyAuthorized();
} else {
notifications.showInformationWhenStartAuthFailed();
} }
}) })
.catch((error) => { .catch((error) => {
console.debug("Error to start auth", { error }) console.debug("Error to start auth", { error });
notifications.showInformationWhenStartAuthFailed();
}); });
}, },
}; };

View File

@ -52,17 +52,17 @@ function showInformationWhenDisconnected() {
}); });
} }
function showInformationStartAuth() { function showInformationStartAuth(callbacks?: { onOpenAuthPage?: () => void }) {
window window
.showInformationMessage( .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", "Continue",
"Settings" "Settings"
) )
.then((selection) => { .then((selection) => {
switch (selection) { switch (selection) {
case "Continue": case "Continue":
commands.executeCommand("tabby.openAuthPage"); commands.executeCommand("tabby.openAuthPage", callbacks);
break; break;
case "Settings": case "Settings":
commands.executeCommand("tabby.openSettings"); commands.executeCommand("tabby.openSettings");
@ -74,6 +74,20 @@ function showInformationAuthSuccess() {
window.showInformationMessage("Congrats, you're authorized, start to use Tabby now."); 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 = { export const notifications = {
showInformationWhenLoading, showInformationWhenLoading,
showInformationWhenDisabled, showInformationWhenDisabled,
@ -81,4 +95,6 @@ export const notifications = {
showInformationWhenDisconnected, showInformationWhenDisconnected,
showInformationStartAuth, showInformationStartAuth,
showInformationAuthSuccess, showInformationAuthSuccess,
showInformationWhenStartAuthButAlreadyAuthorized,
showInformationWhenStartAuthFailed,
}; };

View File

@ -32,11 +32,23 @@ const fsm = createMachine({
entry: () => toDisconnected(), entry: () => toDisconnected(),
}, },
unauthorized: { unauthorized: {
on: { ready: "ready", disconnected: "disconnected", disabled: "disabled" }, on: {
ready: "ready",
disconnected: "disconnected",
disabled: "disabled",
openAuthPage: "unauthorizedAndAuthPageOpen",
},
entry: () => { entry: () => {
toUnauthorized(); toUnauthorized();
notifications.showInformationStartAuth(); notifications.showInformationStartAuth({
onOpenAuthPage: () => {
fsmService.send("openAuthPage");
}, },
});
},
},
unauthorizedAndAuthPageOpen: {
on: { ready: "ready", disconnected: "disconnected", disabled: "disabled" },
exit: (_, event) => { exit: (_, event) => {
if (event.type === "ready") { if (event.type === "ready") {
notifications.showInformationAuthSuccess(); notifications.showInformationAuthSuccess();
@ -79,7 +91,7 @@ function toUnauthorized() {
item.color = colorWarning; item.color = colorWarning;
item.backgroundColor = backgroundColorWarning; item.backgroundColor = backgroundColorWarning;
item.text = `${iconUnauthorized} ${label}`; 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"] }; item.command = { title: "", command: "tabby.statusBarItemClicked", arguments: ["unauthorized"] };
} }