kontexted 0.1.16 → 0.1.17
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.
- package/dist/commands/relogin.d.ts +2 -0
- package/dist/commands/relogin.js +55 -0
- package/dist/index.js +2 -0
- package/dist/lib/api-client.js +9 -5
- package/dist/lib/oauth.js +6 -1
- package/dist/lib/sync/remote-listener.d.ts +1 -0
- package/dist/lib/sync/remote-listener.js +17 -1
- package/dist/lib/sync/sync-engine.d.ts +7 -0
- package/dist/lib/sync/sync-engine.js +38 -0
- package/package.json +4 -4
|
@@ -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);
|
package/dist/lib/api-client.js
CHANGED
|
@@ -112,12 +112,16 @@ export class ApiClient {
|
|
|
112
112
|
logError(`[API CLIENT] Failed to refresh access token - Status: ${response.status}, Error: ${errorText}`);
|
|
113
113
|
return false;
|
|
114
114
|
}
|
|
115
|
-
const
|
|
116
|
-
//
|
|
117
|
-
if (
|
|
118
|
-
|
|
115
|
+
const newTokens = (await response.json());
|
|
116
|
+
// Merge with existing to preserve refresh_token if not returned by server
|
|
117
|
+
if (newTokens.expires_in && !newTokens.expires_at) {
|
|
118
|
+
newTokens.expires_at = Math.floor(Date.now() / 1000) + newTokens.expires_in;
|
|
119
119
|
}
|
|
120
|
-
this.oauth.tokens =
|
|
120
|
+
this.oauth.tokens = {
|
|
121
|
+
...this.oauth.tokens,
|
|
122
|
+
...newTokens,
|
|
123
|
+
refresh_token: newTokens.refresh_token ?? this.oauth.tokens?.refresh_token,
|
|
124
|
+
};
|
|
121
125
|
await this.persist();
|
|
122
126
|
logDebug("[API CLIENT] Token refresh successful");
|
|
123
127
|
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
|
-
|
|
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,
|
|
@@ -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
|
-
//
|
|
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.
|
|
3
|
+
"version": "0.1.17",
|
|
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.
|
|
52
|
-
"@kontexted/linux-x64": "0.1.
|
|
53
|
-
"@kontexted/windows-x64": "0.1.
|
|
51
|
+
"@kontexted/darwin-arm64": "0.1.17",
|
|
52
|
+
"@kontexted/linux-x64": "0.1.17",
|
|
53
|
+
"@kontexted/windows-x64": "0.1.17"
|
|
54
54
|
}
|
|
55
55
|
}
|