kontexted 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerReLoginCommand(program: Command): void;
@@ -0,0 +1,55 @@
1
+ import { readConfig, writeConfig } from "../lib/config.js";
2
+ import { getProfile, addProfile } from "../lib/profile.js";
3
+ import { createOAuthProvider, waitForOAuthCallback } from "../lib/oauth.js";
4
+ import { logDebug } from "../lib/logger.js";
5
+ export function registerReLoginCommand(program) {
6
+ program
7
+ .command("relogin")
8
+ .description("Re-authenticate an existing profile to refresh tokens")
9
+ .requiredOption("--alias <name>", "Profile alias to re-authenticate")
10
+ .action(async (options) => {
11
+ // Read existing config
12
+ const config = await readConfig();
13
+ const existingProfile = getProfile(config, options.alias);
14
+ if (!existingProfile) {
15
+ throw new Error(`Profile not found: ${options.alias}. Use 'kontexted login --url <url> --alias ${options.alias} --workspace <slug>' to create a new profile.`);
16
+ }
17
+ const serverUrl = existingProfile.serverUrl;
18
+ logDebug(`[RELOGIN] Starting re-auth flow - Alias: ${options.alias}, Server: ${serverUrl}`);
19
+ // Create a new OAuth state that preserves client information from existing profile
20
+ const oauth = {
21
+ clientInformation: existingProfile.oauth?.clientInformation,
22
+ clientMetadata: existingProfile.oauth?.clientMetadata,
23
+ };
24
+ const persist = async () => {
25
+ // Update the profile in config with new OAuth state
26
+ const updatedProfile = {
27
+ ...existingProfile,
28
+ oauth,
29
+ };
30
+ addProfile(config, options.alias, updatedProfile);
31
+ await writeConfig(config);
32
+ };
33
+ // Create OAuth provider with existing client info
34
+ const provider = createOAuthProvider(oauth, persist, serverUrl);
35
+ // 1. If no client information exists, register a new OAuth client
36
+ if (!oauth.clientInformation) {
37
+ logDebug(`[RELOGIN] No existing client information, registering new OAuth client...`);
38
+ await provider.registerClient();
39
+ }
40
+ else {
41
+ logDebug(`[RELOGIN] Using existing client information: ${oauth.clientInformation.client_id}`);
42
+ }
43
+ // 2. Get authorization URL and open browser
44
+ const authUrl = await provider.getAuthorizationUrl();
45
+ logDebug(`[RELOGIN] Opening browser for authorization...`);
46
+ provider.redirectToAuthorization(authUrl);
47
+ // 3. Wait for callback with auth code
48
+ logDebug(`[RELOGIN] Waiting for authorization callback...`);
49
+ const authCode = await waitForOAuthCallback();
50
+ // 4. Exchange code for tokens
51
+ logDebug(`[RELOGIN] Exchanging authorization code for tokens...`);
52
+ await provider.exchangeCodeForToken(authCode);
53
+ console.log(`✓ Re-authentication successful. Tokens refreshed for profile: ${options.alias}`);
54
+ });
55
+ }
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { dirname, join } from "node:path";
5
5
  import { Command } from "commander";
6
6
  import { registerLoginCommand } from "./commands/login.js";
7
+ import { registerReLoginCommand } from "./commands/relogin.js";
7
8
  import { registerLogoutCommand } from "./commands/logout.js";
8
9
  import { registerShowConfigCommand } from "./commands/show-config.js";
9
10
  import { registerMcpCommand } from "./commands/mcp.js";
@@ -20,6 +21,7 @@ program
20
21
  .version(packageJson.version);
21
22
  // Register subcommands
22
23
  registerLoginCommand(program);
24
+ registerReLoginCommand(program);
23
25
  registerLogoutCommand(program);
24
26
  registerShowConfigCommand(program);
25
27
  registerSkillCommand(program);
@@ -104,6 +104,7 @@ export class ApiClient {
104
104
  refresh_token: this.oauth.tokens.refresh_token,
105
105
  client_id: this.oauth.clientInformation?.client_id ?? "",
106
106
  client_secret: this.oauth.clientInformation?.client_secret ?? "",
107
+ resource: this.serverUrl,
107
108
  }),
108
109
  });
109
110
  logDebug(`[API CLIENT] Token refresh response status: ${response.status}`);
@@ -112,12 +113,16 @@ export class ApiClient {
112
113
  logError(`[API CLIENT] Failed to refresh access token - Status: ${response.status}, Error: ${errorText}`);
113
114
  return false;
114
115
  }
115
- const tokens = (await response.json());
116
- // Calculate absolute expiry time if not provided by server
117
- if (tokens.expires_in && !tokens.expires_at) {
118
- tokens.expires_at = Math.floor(Date.now() / 1000) + tokens.expires_in;
116
+ const newTokens = (await response.json());
117
+ // Merge with existing to preserve refresh_token if not returned by server
118
+ if (newTokens.expires_in && !newTokens.expires_at) {
119
+ newTokens.expires_at = Math.floor(Date.now() / 1000) + newTokens.expires_in;
119
120
  }
120
- this.oauth.tokens = tokens;
121
+ this.oauth.tokens = {
122
+ ...this.oauth.tokens,
123
+ ...newTokens,
124
+ refresh_token: newTokens.refresh_token ?? this.oauth.tokens?.refresh_token,
125
+ };
121
126
  await this.persist();
122
127
  logDebug("[API CLIENT] Token refresh successful");
123
128
  return true;
package/dist/lib/oauth.js CHANGED
@@ -347,7 +347,12 @@ export async function ensureValidTokens(oauth, persist, serverUrl) {
347
347
  if (newTokens.expires_in && !newTokens.expires_at) {
348
348
  newTokens.expires_at = Math.floor(Date.now() / 1000) + newTokens.expires_in;
349
349
  }
350
- oauth.tokens = newTokens;
350
+ // Merge with existing to preserve refresh_token if not returned by server
351
+ oauth.tokens = {
352
+ ...oauth.tokens,
353
+ ...newTokens,
354
+ refresh_token: newTokens.refresh_token ?? oauth.tokens?.refresh_token,
355
+ };
351
356
  await persist();
352
357
  logInfo("Token refresh successful", {
353
358
  newExpiresAt: newTokens.expires_at,
@@ -13,6 +13,7 @@ export declare class RemoteListener {
13
13
  private reconnectAttempts;
14
14
  private maxReconnectAttempts;
15
15
  private reconnectDelay;
16
+ private readonly TOKEN_REFRESH_BUFFER_SECONDS;
16
17
  private isRunning;
17
18
  private reconnectTimeout;
18
19
  private onStopped?;
@@ -11,6 +11,7 @@ export class RemoteListener {
11
11
  reconnectAttempts = 0;
12
12
  maxReconnectAttempts = 10;
13
13
  reconnectDelay = 1000;
14
+ TOKEN_REFRESH_BUFFER_SECONDS = 5 * 60; // Refresh 5 minutes before expiry
14
15
  isRunning = false;
15
16
  reconnectTimeout = null;
16
17
  onStopped;
@@ -42,7 +43,22 @@ export class RemoteListener {
42
43
  }
43
44
  const url = `${this.apiClient.baseUrl}/api/sync/events?workspaceSlug=${encodeURIComponent(this.workspaceSlug)}`;
44
45
  this.abortController = new AbortController();
45
- // Get fresh token before connecting
46
+ // Proactively check and refresh token BEFORE connecting
47
+ const tokens = this.apiClient.getOAuth().tokens;
48
+ const now = Math.floor(Date.now() / 1000);
49
+ // Check if token is missing, expired, or expiring within buffer time
50
+ const needsRefresh = !tokens?.access_token ||
51
+ (tokens.expires_at && tokens.expires_at <= now + this.TOKEN_REFRESH_BUFFER_SECONDS);
52
+ if (needsRefresh) {
53
+ console.log("[RemoteListener] Token expired or expiring soon, refreshing before connect...");
54
+ const refreshed = await this.apiClient.refreshToken();
55
+ if (!refreshed) {
56
+ console.error("[RemoteListener] Failed to refresh token before connect");
57
+ this.handleReconnect();
58
+ return;
59
+ }
60
+ }
61
+ // Get fresh token after potential refresh
46
62
  let accessToken = this.apiClient.getAccessToken();
47
63
  if (!accessToken) {
48
64
  console.error("[RemoteListener] No access token available");
@@ -23,6 +23,9 @@ export declare class SyncEngine {
23
23
  private state;
24
24
  private running;
25
25
  private paused;
26
+ private tokenRefreshInterval;
27
+ private readonly TOKEN_REFRESH_CHECK_INTERVAL_MS;
28
+ private readonly TOKEN_REFRESH_BUFFER_SECONDS;
26
29
  constructor(syncDir: string, apiClient: ApiClient);
27
30
  /**
28
31
  * Check if pause flag file exists
@@ -33,6 +36,10 @@ export declare class SyncEngine {
33
36
  * Performs initial sync first, then starts file watcher
34
37
  */
35
38
  start(): Promise<void>;
39
+ /**
40
+ * Start periodic token refresh to prevent auth expiry during long-running sessions
41
+ */
42
+ private startTokenRefreshTimer;
36
43
  /**
37
44
  * Stop the sync engine
38
45
  * Stops file watcher and closes queue database
@@ -37,6 +37,9 @@ export class SyncEngine {
37
37
  state;
38
38
  running = false;
39
39
  paused = false;
40
+ tokenRefreshInterval = null;
41
+ TOKEN_REFRESH_CHECK_INTERVAL_MS = 30 * 60 * 1000; // Check every 30 minutes
42
+ TOKEN_REFRESH_BUFFER_SECONDS = 10 * 60; // Refresh 10 minutes before expiry
40
43
  constructor(syncDir, apiClient) {
41
44
  this.syncDir = syncDir;
42
45
  this.apiClient = apiClient;
@@ -93,14 +96,49 @@ export class SyncEngine {
93
96
  this.fileWatcher.start();
94
97
  // Start listening for remote changes
95
98
  await this.remoteListener.start();
99
+ // Start periodic token refresh to prevent auth expiry
100
+ this.startTokenRefreshTimer();
96
101
  console.log("Sync engine started");
97
102
  }
103
+ /**
104
+ * Start periodic token refresh to prevent auth expiry during long-running sessions
105
+ */
106
+ startTokenRefreshTimer() {
107
+ this.tokenRefreshInterval = setInterval(async () => {
108
+ if (!this.running)
109
+ return;
110
+ try {
111
+ const tokens = this.apiClient.getOAuth().tokens;
112
+ const now = Math.floor(Date.now() / 1000);
113
+ // Check if token is expired or expiring soon
114
+ if (!tokens?.expires_at || tokens.expires_at <= now + this.TOKEN_REFRESH_BUFFER_SECONDS) {
115
+ console.log("[SyncEngine] Token expiring soon, refreshing proactively...");
116
+ const refreshed = await this.apiClient.refreshToken();
117
+ if (!refreshed) {
118
+ console.error("[SyncEngine] Proactive token refresh failed - will retry on next check");
119
+ }
120
+ else {
121
+ console.log("[SyncEngine] Token refreshed successfully");
122
+ }
123
+ }
124
+ }
125
+ catch (error) {
126
+ console.error("[SyncEngine] Token refresh check error:", error);
127
+ }
128
+ }, this.TOKEN_REFRESH_CHECK_INTERVAL_MS);
129
+ console.log("[SyncEngine] Token refresh timer started");
130
+ }
98
131
  /**
99
132
  * Stop the sync engine
100
133
  * Stops file watcher and closes queue database
101
134
  */
102
135
  stop() {
103
136
  this.running = false;
137
+ // Stop token refresh timer
138
+ if (this.tokenRefreshInterval) {
139
+ clearInterval(this.tokenRefreshInterval);
140
+ this.tokenRefreshInterval = null;
141
+ }
104
142
  this.fileWatcher.stop();
105
143
  this.remoteListener.stop();
106
144
  this.queue.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kontexted",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "CLI tool for Kontexted - MCP proxy, workspaces management, and local server",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,8 +48,8 @@
48
48
  "typescript": "^5.6.0"
49
49
  },
50
50
  "optionalDependencies": {
51
- "@kontexted/darwin-arm64": "0.1.16",
52
- "@kontexted/linux-x64": "0.1.16",
53
- "@kontexted/windows-x64": "0.1.16"
51
+ "@kontexted/darwin-arm64": "0.1.18",
52
+ "@kontexted/linux-x64": "0.1.18",
53
+ "@kontexted/windows-x64": "0.1.18"
54
54
  }
55
55
  }