@unifiedmemory/cli 1.3.13 ā 1.3.14
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/.env.example +10 -0
- package/README.md +28 -0
- package/commands/debug.js +67 -0
- package/commands/init.js +17 -8
- package/commands/login.js +46 -94
- package/commands/org.js +25 -177
- package/commands/record.js +14 -11
- package/commands/usage.js +163 -0
- package/index.js +37 -6
- package/lib/clerk-api.js +9 -12
- package/lib/config.js +11 -1
- package/lib/mcp-proxy.js +9 -10
- package/lib/mcp-server.js +139 -34
- package/lib/token-refresh.js +10 -24
- package/lib/token-storage.js +33 -27
- package/lib/token-validation.js +2 -85
- package/package.json +1 -1
- package/tests/unit/mcp-proxy.test.js +9 -6
- package/tests/unit/token-refresh.test.js +18 -44
- package/tests/unit/token-storage.test.js +1 -2
- package/tests/unit/token-validation.test.js +24 -241
package/.env.example
CHANGED
|
@@ -45,6 +45,16 @@
|
|
|
45
45
|
# Default: https://unifiedmemory.ai/oauth/callback
|
|
46
46
|
# OAUTH_SUCCESS_URL=https://unifiedmemory.ai/oauth/callback
|
|
47
47
|
|
|
48
|
+
# ============================================
|
|
49
|
+
# Frontend URL (CLI Auth Page)
|
|
50
|
+
# ============================================
|
|
51
|
+
# URL of the frontend app that hosts the CLI auth page with org picker
|
|
52
|
+
# During login, the browser opens this URL where users can select an organization
|
|
53
|
+
# before being redirected to Clerk OAuth
|
|
54
|
+
# Default: https://unifiedmemory.ai
|
|
55
|
+
# For local development: http://localhost:3000
|
|
56
|
+
# FRONTEND_URL=https://unifiedmemory.ai
|
|
57
|
+
|
|
48
58
|
# ============================================
|
|
49
59
|
# Clerk Client Secret (Optional)
|
|
50
60
|
# ============================================
|
package/README.md
CHANGED
|
@@ -106,6 +106,34 @@ Project Configuration:
|
|
|
106
106
|
ā Configured: My Project (proj_xxx)
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
### `um usage`
|
|
110
|
+
Check your current usage and quota allowances.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
um usage
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Example output:**
|
|
117
|
+
```
|
|
118
|
+
š Usage & Quota
|
|
119
|
+
|
|
120
|
+
Account:
|
|
121
|
+
Organization: My Team
|
|
122
|
+
|
|
123
|
+
Monthly Queries:
|
|
124
|
+
450 / 1,000 (45.0%)
|
|
125
|
+
[āāāāāāāāāāāāāāāāāāāā]
|
|
126
|
+
Remaining: 550 queries
|
|
127
|
+
Resets: 2/1/2026
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Context-aware: Shows personal or organization quota based on current context (`um org switch`).
|
|
131
|
+
|
|
132
|
+
**Color indicators:**
|
|
133
|
+
- š¢ Green: <70% usage
|
|
134
|
+
- š” Yellow: 70-89% usage
|
|
135
|
+
- š“ Red: ā„90% usage (with upgrade prompt)
|
|
136
|
+
|
|
109
137
|
### `um org switch`
|
|
110
138
|
Switch between your organizations or personal account.
|
|
111
139
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getToken, getOrgContext } from '../lib/token-storage.js';
|
|
3
|
+
import { isTokenExpired } from '../lib/token-refresh.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Debug authentication and token issues
|
|
7
|
+
* Displays comprehensive diagnostics about the current token state
|
|
8
|
+
* JWT is the single source of truth for org context
|
|
9
|
+
*/
|
|
10
|
+
export async function debugAuth() {
|
|
11
|
+
console.log(chalk.blue('\nš Token Diagnostics\n'));
|
|
12
|
+
|
|
13
|
+
const tokenData = getToken();
|
|
14
|
+
|
|
15
|
+
if (!tokenData) {
|
|
16
|
+
console.log(chalk.red('ā No token found'));
|
|
17
|
+
console.log(chalk.gray(' Run: um login'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check expiration
|
|
22
|
+
const expired = isTokenExpired(tokenData);
|
|
23
|
+
console.log(chalk.yellow('Token Expiration:'));
|
|
24
|
+
console.log(expired ? chalk.red(' ā Expired') : chalk.green(' ā Valid'));
|
|
25
|
+
if (tokenData.decoded?.exp) {
|
|
26
|
+
const expDate = new Date(tokenData.decoded.exp * 1000);
|
|
27
|
+
console.log(chalk.gray(` Expires: ${expDate.toLocaleString()}`));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check org context from JWT claims (single source of truth)
|
|
31
|
+
console.log(chalk.yellow('\nOrganization Context (from JWT):'));
|
|
32
|
+
const orgContext = getOrgContext();
|
|
33
|
+
|
|
34
|
+
if (orgContext) {
|
|
35
|
+
console.log(chalk.green(` ā Organization: ${orgContext.name} (${orgContext.id})`));
|
|
36
|
+
console.log(chalk.gray(` Role: ${orgContext.role || 'member'}`));
|
|
37
|
+
if (orgContext.slug) {
|
|
38
|
+
console.log(chalk.gray(` Slug: ${orgContext.slug}`));
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
console.log(chalk.cyan(' Personal Account (no org claims in JWT)'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check session ID
|
|
45
|
+
console.log(chalk.yellow('\nSession ID:'));
|
|
46
|
+
const hasSid = tokenData.decoded?.sid;
|
|
47
|
+
console.log(hasSid ? chalk.green(' ā Present') : chalk.yellow(' ā Missing'));
|
|
48
|
+
if (hasSid) {
|
|
49
|
+
console.log(chalk.gray(` SID: ${tokenData.decoded.sid}`));
|
|
50
|
+
} else {
|
|
51
|
+
console.log(chalk.gray(' Note: Some org-scoped tokens may not have sid'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check user ID
|
|
55
|
+
console.log(chalk.yellow('\nUser ID:'));
|
|
56
|
+
if (tokenData.decoded?.sub) {
|
|
57
|
+
console.log(chalk.green(` ā ${tokenData.decoded.sub}`));
|
|
58
|
+
} else {
|
|
59
|
+
console.log(chalk.red(' ā Missing'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check refresh token
|
|
63
|
+
console.log(chalk.yellow('\nRefresh Token:'));
|
|
64
|
+
console.log(tokenData.refresh_token ? chalk.green(' ā Present') : chalk.red(' ā Missing'));
|
|
65
|
+
|
|
66
|
+
console.log('');
|
|
67
|
+
}
|
package/commands/init.js
CHANGED
|
@@ -65,8 +65,9 @@ export async function init(options = {}) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
console.log(chalk.green(`ā Logged in as: ${authData.user_id}`));
|
|
68
|
-
if (authData.org_id) {
|
|
69
|
-
|
|
68
|
+
if (authData.org_id && authData.org_id !== authData.user_id) {
|
|
69
|
+
const orgDisplay = authData.org_name || authData.org_id;
|
|
70
|
+
console.log(chalk.green(`ā Organization: ${orgDisplay}`));
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
// Step 1.5: Check for existing config
|
|
@@ -166,14 +167,17 @@ async function ensureAuthenticated(options) {
|
|
|
166
167
|
if (stored && stored.decoded) {
|
|
167
168
|
console.log(chalk.gray('Using saved session'));
|
|
168
169
|
|
|
169
|
-
// Extract user_id and org_id from
|
|
170
|
+
// Extract user_id and org_id from JWT claims, with selectedOrgId fallback
|
|
170
171
|
const userId = stored.decoded.sub;
|
|
171
|
-
|
|
172
|
+
// Use org from JWT o.* claims, fallback to selectedOrgId from login, then userId for personal context
|
|
173
|
+
const orgId = stored.decoded.o?.o_id || stored.selectedOrgId || userId;
|
|
174
|
+
const orgName = stored.decoded.o?.o_name || stored.selectedOrgName || null;
|
|
172
175
|
const expirationTime = stored.decoded.exp * 1000;
|
|
173
176
|
|
|
174
177
|
return {
|
|
175
178
|
user_id: userId,
|
|
176
179
|
org_id: orgId,
|
|
180
|
+
org_name: orgName,
|
|
177
181
|
access_token: stored.idToken || stored.accessToken,
|
|
178
182
|
api_url: 'https://rose-asp-main-1c0b114.d2.zuplo.dev',
|
|
179
183
|
expires_at: expirationTime,
|
|
@@ -189,14 +193,17 @@ async function ensureAuthenticated(options) {
|
|
|
189
193
|
return null;
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
// Extract from saved token
|
|
196
|
+
// Extract from saved token (JWT claims with selectedOrgId fallback)
|
|
193
197
|
const savedToken = getToken();
|
|
194
198
|
const userId = savedToken?.decoded?.sub;
|
|
195
|
-
|
|
199
|
+
// Use org from JWT o.* claims, fallback to selectedOrgId from login, then userId for personal context
|
|
200
|
+
const orgId = savedToken?.decoded?.o?.o_id || savedToken?.selectedOrgId || userId;
|
|
201
|
+
const orgName = savedToken?.decoded?.o?.o_name || savedToken?.selectedOrgName || null;
|
|
196
202
|
|
|
197
203
|
return {
|
|
198
204
|
user_id: userId,
|
|
199
205
|
org_id: orgId,
|
|
206
|
+
org_name: orgName,
|
|
200
207
|
access_token: savedToken.idToken || savedToken.accessToken,
|
|
201
208
|
api_url: 'https://rose-asp-main-1c0b114.d2.zuplo.dev',
|
|
202
209
|
expires_at: savedToken.decoded?.exp * 1000,
|
|
@@ -244,10 +251,12 @@ async function selectOrCreateProject(authData, options) {
|
|
|
244
251
|
return null;
|
|
245
252
|
}
|
|
246
253
|
|
|
247
|
-
// Update authData with fresh credentials
|
|
254
|
+
// Update authData with fresh credentials (JWT claims with selectedOrgId fallback)
|
|
248
255
|
const savedToken = getToken();
|
|
249
256
|
authData.user_id = savedToken.decoded.sub;
|
|
250
|
-
|
|
257
|
+
// Use org from JWT o.* claims, fallback to selectedOrgId from login, then userId for personal context
|
|
258
|
+
authData.org_id = savedToken.decoded?.o?.o_id || savedToken?.selectedOrgId || authData.user_id;
|
|
259
|
+
authData.org_name = savedToken.decoded?.o?.o_name || savedToken?.selectedOrgName || null;
|
|
251
260
|
authData.access_token = savedToken.idToken || savedToken.accessToken;
|
|
252
261
|
authData.expires_at = savedToken.decoded?.exp * 1000;
|
|
253
262
|
|
package/commands/login.js
CHANGED
|
@@ -3,12 +3,9 @@ import { URL } from 'url';
|
|
|
3
3
|
import open from 'open';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
|
-
import inquirer from 'inquirer';
|
|
7
6
|
import { config, validateConfig } from '../lib/config.js';
|
|
8
|
-
import { saveToken
|
|
9
|
-
import { getUserOrganizations, getOrganizationsFromToken, getOrgScopedToken } from '../lib/clerk-api.js';
|
|
7
|
+
import { saveToken } from '../lib/token-storage.js';
|
|
10
8
|
import { parseJWT } from '../lib/jwt-utils.js';
|
|
11
|
-
import { promptOrganizationSelection, displayOrganizationSelection } from '../lib/org-selection-ui.js';
|
|
12
9
|
|
|
13
10
|
function generateRandomState() {
|
|
14
11
|
// Use cryptographically secure random bytes for CSRF protection
|
|
@@ -38,11 +35,10 @@ export async function login() {
|
|
|
38
35
|
const state = generateRandomState();
|
|
39
36
|
const pkce = generatePKCE();
|
|
40
37
|
|
|
41
|
-
// Build
|
|
42
|
-
const authUrl = new URL(
|
|
38
|
+
// Build URL to frontend CLI auth page (which shows org picker before Clerk OAuth)
|
|
39
|
+
const authUrl = new URL(`${config.frontendUrl}/cli-auth`);
|
|
43
40
|
authUrl.searchParams.append('client_id', config.clerkClientId);
|
|
44
41
|
authUrl.searchParams.append('redirect_uri', config.redirectUri);
|
|
45
|
-
authUrl.searchParams.append('response_type', 'code');
|
|
46
42
|
authUrl.searchParams.append('scope', 'openid profile email');
|
|
47
43
|
authUrl.searchParams.append('state', state);
|
|
48
44
|
authUrl.searchParams.append('code_challenge', pkce.challenge);
|
|
@@ -76,7 +72,23 @@ export async function login() {
|
|
|
76
72
|
return;
|
|
77
73
|
}
|
|
78
74
|
|
|
79
|
-
|
|
75
|
+
// Parse state to extract CSRF token, org_id, and org_name
|
|
76
|
+
// State format: base64({ csrf: string, org_id: string|null, org_name: string|null })
|
|
77
|
+
let csrfState = returnedState;
|
|
78
|
+
let selectedOrgId = null;
|
|
79
|
+
let selectedOrgName = null;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const stateData = JSON.parse(atob(returnedState));
|
|
83
|
+
csrfState = stateData.csrf;
|
|
84
|
+
selectedOrgId = stateData.org_id;
|
|
85
|
+
selectedOrgName = stateData.org_name;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
// Old format - state is just the CSRF token
|
|
88
|
+
csrfState = returnedState;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (csrfState !== state) {
|
|
80
92
|
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
81
93
|
res.end(`
|
|
82
94
|
<html>
|
|
@@ -242,10 +254,12 @@ export async function login() {
|
|
|
242
254
|
`);
|
|
243
255
|
|
|
244
256
|
// Parse JWT for user info - do not log tokens
|
|
257
|
+
// The id_token should already have org claims because the frontend
|
|
258
|
+
// called setActive() before OAuth redirect
|
|
245
259
|
const tokenToParse = tokenData.id_token || tokenData.access_token;
|
|
246
260
|
const decoded = parseJWT(tokenToParse);
|
|
247
261
|
|
|
248
|
-
// Save token (
|
|
262
|
+
// Save token with selected org from state (since JWT doesn't have org claims)
|
|
249
263
|
saveToken({
|
|
250
264
|
accessToken: tokenData.access_token,
|
|
251
265
|
idToken: tokenData.id_token,
|
|
@@ -253,7 +267,10 @@ export async function login() {
|
|
|
253
267
|
tokenType: tokenData.token_type || 'Bearer',
|
|
254
268
|
expiresIn: tokenData.expires_in,
|
|
255
269
|
receivedAt: Date.now(),
|
|
256
|
-
decoded: decoded
|
|
270
|
+
decoded: decoded,
|
|
271
|
+
sessionId: decoded?.sid,
|
|
272
|
+
selectedOrgId: selectedOrgId, // From state parameter
|
|
273
|
+
selectedOrgName: selectedOrgName, // From state parameter
|
|
257
274
|
});
|
|
258
275
|
|
|
259
276
|
console.log(chalk.green('\nā
Authentication successful!'));
|
|
@@ -269,95 +286,30 @@ export async function login() {
|
|
|
269
286
|
}
|
|
270
287
|
}
|
|
271
288
|
|
|
272
|
-
// Close server
|
|
273
|
-
server.close(
|
|
289
|
+
// Close server and display org context
|
|
290
|
+
server.close(() => {
|
|
274
291
|
console.log(chalk.gray('ā Callback server closed'));
|
|
275
292
|
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
console.log(chalk.blue('\nš Checking for organizations...'));
|
|
290
|
-
const selectedOrg = await promptOrganizationSelection(memberships);
|
|
291
|
-
displayOrganizationSelection(selectedOrg);
|
|
292
|
-
|
|
293
|
-
if (selectedOrg) {
|
|
294
|
-
// Get org-scoped JWT from Clerk
|
|
295
|
-
try {
|
|
296
|
-
console.log(chalk.cyan('\nš Getting organization-scoped token...'));
|
|
297
|
-
|
|
298
|
-
const sessionId = decoded.sid;
|
|
299
|
-
if (!sessionId) {
|
|
300
|
-
throw new Error('No session ID found in token');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const orgToken = await getOrgScopedToken(
|
|
304
|
-
sessionId,
|
|
305
|
-
selectedOrg.id,
|
|
306
|
-
tokenData.id_token
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
// Update saved token with org-scoped version
|
|
310
|
-
// Preserve originalSid for recovery purposes (org-scoped token won't have sid)
|
|
311
|
-
saveToken({
|
|
312
|
-
accessToken: tokenData.access_token,
|
|
313
|
-
idToken: orgToken.jwt,
|
|
314
|
-
refresh_token: tokenData.refresh_token,
|
|
315
|
-
tokenType: 'Bearer',
|
|
316
|
-
expiresIn: tokenData.expires_in,
|
|
317
|
-
receivedAt: Date.now(),
|
|
318
|
-
decoded: parseJWT(orgToken.jwt),
|
|
319
|
-
selectedOrg: selectedOrg,
|
|
320
|
-
originalSid: decoded.sid // Preserve session ID from original OAuth token
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
console.log(chalk.green(`\nā
Using organization context: ${chalk.bold(selectedOrg.name)}`));
|
|
324
|
-
console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
|
|
325
|
-
console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
|
|
326
|
-
console.log(chalk.gray(' ā Token updated with organization context'));
|
|
327
|
-
} catch (error) {
|
|
328
|
-
console.error(chalk.yellow('\nā ļø Failed to get org-scoped token:'), error.message);
|
|
329
|
-
console.log(chalk.gray(' Continuing with original token (may have limited org access)'));
|
|
330
|
-
|
|
331
|
-
// Save token with selectedOrg AND originalSid for recovery
|
|
332
|
-
// This allows token-validation.js to retry getting org-scoped token later
|
|
333
|
-
saveToken({
|
|
334
|
-
accessToken: tokenData.access_token,
|
|
335
|
-
idToken: tokenData.id_token,
|
|
336
|
-
refresh_token: tokenData.refresh_token,
|
|
337
|
-
tokenType: 'Bearer',
|
|
338
|
-
expiresIn: tokenData.expires_in,
|
|
339
|
-
receivedAt: Date.now(),
|
|
340
|
-
decoded: decoded,
|
|
341
|
-
selectedOrg: selectedOrg,
|
|
342
|
-
originalSid: decoded.sid // Preserve session ID for recovery
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
console.log(chalk.green(`\nā
Using organization context: ${chalk.bold(selectedOrg.name)}`));
|
|
346
|
-
console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
|
|
347
|
-
console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
|
|
348
|
-
console.log(chalk.yellow(' ā ļø Token lacks org claims - recovery will be attempted on next use'));
|
|
349
|
-
}
|
|
350
|
-
} else {
|
|
351
|
-
console.log(chalk.green('\nā
Using personal account context'));
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
console.log(chalk.gray('\nYou can switch organizations anytime with: um org switch'));
|
|
293
|
+
// Display organization context - prefer JWT claims, fall back to selected org from state
|
|
294
|
+
// Support both flat claims (org_id) and nested claims (o.o_id) from Clerk JWT templates
|
|
295
|
+
const jwtOrgId = decoded?.org_id || decoded?.o?.o_id;
|
|
296
|
+
const orgId = jwtOrgId || selectedOrgId;
|
|
297
|
+
const orgName = decoded?.org_name || decoded?.o?.o_name || selectedOrgName;
|
|
298
|
+
const orgSlug = decoded?.org_slug || decoded?.o?.o_slug;
|
|
299
|
+
const orgRole = decoded?.org_role || decoded?.o?.o_role;
|
|
300
|
+
|
|
301
|
+
if (orgId) {
|
|
302
|
+
console.log(chalk.green(`\nā
Organization context: ${chalk.bold(orgName || orgSlug || orgId)}`));
|
|
303
|
+
console.log(chalk.gray(` Organization ID: ${orgId}`));
|
|
304
|
+
if (orgRole) {
|
|
305
|
+
console.log(chalk.gray(` Your role: ${orgRole}`));
|
|
355
306
|
}
|
|
356
|
-
}
|
|
357
|
-
console.log(chalk.
|
|
358
|
-
console.log(chalk.gray(`Error: ${error.message}`));
|
|
307
|
+
} else {
|
|
308
|
+
console.log(chalk.green('\nā
Using personal account context'));
|
|
359
309
|
}
|
|
360
310
|
|
|
311
|
+
console.log(chalk.gray('\nYou can switch organizations anytime with: um org switch'));
|
|
312
|
+
|
|
361
313
|
resolve(tokenData);
|
|
362
314
|
});
|
|
363
315
|
} catch (error) {
|
package/commands/org.js
CHANGED
|
@@ -1,115 +1,30 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
3
|
-
import { loadAndRefreshToken } from '../lib/token-validation.js';
|
|
4
|
-
import { refreshAccessToken } from '../lib/token-refresh.js';
|
|
5
|
-
import { getUserOrganizations, getOrganizationsFromToken, getOrgScopedToken } from '../lib/clerk-api.js';
|
|
6
|
-
import { parseJWT } from '../lib/jwt-utils.js';
|
|
7
|
-
import { promptOrganizationSelection, displayOrganizationSelection } from '../lib/org-selection-ui.js';
|
|
2
|
+
import { getOrgContext } from '../lib/token-storage.js';
|
|
8
3
|
|
|
9
4
|
/**
|
|
10
5
|
* Switch organization context
|
|
6
|
+
* Re-runs the login flow which shows the org picker on the frontend
|
|
11
7
|
*/
|
|
12
8
|
export async function switchOrg() {
|
|
13
|
-
|
|
14
|
-
const tokenData = await loadAndRefreshToken();
|
|
9
|
+
console.log(chalk.blue('\nš Opening browser to select organization...\n'));
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
if (!userId || !accessToken) {
|
|
20
|
-
console.error(chalk.red('ā Invalid session'));
|
|
21
|
-
console.log(chalk.gray('Run `um login` to re-authenticate'));
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
console.log(chalk.blue('\nš Fetching organizations...'));
|
|
26
|
-
|
|
27
|
-
// First try to get organizations from JWT token
|
|
28
|
-
let memberships = tokenData.decoded
|
|
29
|
-
? getOrganizationsFromToken(tokenData.decoded)
|
|
30
|
-
: [];
|
|
31
|
-
|
|
32
|
-
// If not in JWT, fetch from Clerk Frontend API
|
|
33
|
-
if (memberships.length === 0) {
|
|
34
|
-
const sessionToken = accessToken;
|
|
35
|
-
memberships = await getUserOrganizations(userId, sessionToken);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (memberships.length === 0) {
|
|
39
|
-
console.log(chalk.yellow('\nā ļø No organizations found'));
|
|
40
|
-
console.log(chalk.gray('You are using a personal account context.'));
|
|
41
|
-
console.log(chalk.gray('Create an organization at https://unifiedmemory.ai to collaborate with your team.'));
|
|
42
|
-
process.exit(0);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Get current selection
|
|
46
|
-
const currentOrg = getSelectedOrg();
|
|
47
|
-
|
|
48
|
-
// Prompt user to select
|
|
49
|
-
const selectedOrg = await promptOrganizationSelection(memberships, currentOrg);
|
|
50
|
-
|
|
51
|
-
// Update selected organization with org-scoped token
|
|
52
|
-
if (selectedOrg) {
|
|
53
|
-
try {
|
|
54
|
-
// Get session ID from current token
|
|
55
|
-
const sessionId = tokenData.decoded?.sid || tokenData.originalSid;
|
|
56
|
-
const currentToken = tokenData.idToken || tokenData.accessToken;
|
|
57
|
-
|
|
58
|
-
if (sessionId) {
|
|
59
|
-
console.log(chalk.cyan('\nš Getting organization-scoped token...'));
|
|
60
|
-
const orgToken = await getOrgScopedToken(sessionId, selectedOrg.id, currentToken);
|
|
61
|
-
|
|
62
|
-
// Update with org-scoped token
|
|
63
|
-
saveToken({
|
|
64
|
-
...tokenData,
|
|
65
|
-
idToken: orgToken.jwt,
|
|
66
|
-
decoded: parseJWT(orgToken.jwt),
|
|
67
|
-
selectedOrg: selectedOrg,
|
|
68
|
-
originalSid: sessionId
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
console.log(chalk.green(`\nā
Switched to organization: ${chalk.bold(selectedOrg.name)}`));
|
|
72
|
-
console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
|
|
73
|
-
console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
|
|
74
|
-
console.log(chalk.gray(' ā Token updated with organization context'));
|
|
75
|
-
} else {
|
|
76
|
-
// Fall back to just updating selectedOrg (recovery will try later)
|
|
77
|
-
updateSelectedOrg(selectedOrg);
|
|
78
|
-
console.log(chalk.green(`\nā
Switched to organization: ${chalk.bold(selectedOrg.name)}`));
|
|
79
|
-
console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
|
|
80
|
-
console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
|
|
81
|
-
console.log(chalk.yellow(' ā ļø No session ID available - token lacks org claims'));
|
|
82
|
-
console.log(chalk.gray(' Try: um login'));
|
|
83
|
-
}
|
|
84
|
-
} catch (error) {
|
|
85
|
-
console.error(chalk.yellow(`\nā ļø Failed to get org-scoped token: ${error.message}`));
|
|
86
|
-
updateSelectedOrg(selectedOrg);
|
|
87
|
-
console.log(chalk.green(`\nā
Switched to organization: ${chalk.bold(selectedOrg.name)}`));
|
|
88
|
-
console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
|
|
89
|
-
console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
|
|
90
|
-
console.log(chalk.gray(' Saved org selection, but token may need refresh'));
|
|
91
|
-
console.log(chalk.gray(' Try: um org fix'));
|
|
92
|
-
}
|
|
93
|
-
} else {
|
|
94
|
-
updateSelectedOrg(null);
|
|
95
|
-
console.log(chalk.green('\nā
Switched to personal account context'));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
console.log(chalk.gray('\nRun `um status` to verify your current context'));
|
|
11
|
+
// Re-use login flow - frontend will show org picker
|
|
12
|
+
const { login } = await import('./login.js');
|
|
13
|
+
await login();
|
|
99
14
|
}
|
|
100
15
|
|
|
101
16
|
/**
|
|
102
|
-
* Show current organization
|
|
17
|
+
* Show current organization context from JWT
|
|
103
18
|
*/
|
|
104
19
|
export async function showOrg() {
|
|
105
|
-
const
|
|
20
|
+
const orgContext = getOrgContext();
|
|
106
21
|
|
|
107
22
|
console.log(chalk.blue('\nš Current Organization Context\n'));
|
|
108
23
|
|
|
109
|
-
if (
|
|
110
|
-
console.log(chalk.green(` ${
|
|
111
|
-
console.log(chalk.gray(` ID: ${
|
|
112
|
-
console.log(chalk.gray(` Role: ${
|
|
24
|
+
if (orgContext) {
|
|
25
|
+
console.log(chalk.green(` ${orgContext.name}${orgContext.slug ? ` (${orgContext.slug})` : ''}`));
|
|
26
|
+
console.log(chalk.gray(` ID: ${orgContext.id}`));
|
|
27
|
+
console.log(chalk.gray(` Role: ${orgContext.role || 'member'}`));
|
|
113
28
|
} else {
|
|
114
29
|
console.log(chalk.cyan(' Personal Account'));
|
|
115
30
|
console.log(chalk.gray(' (no organization selected)'));
|
|
@@ -119,90 +34,23 @@ export async function showOrg() {
|
|
|
119
34
|
}
|
|
120
35
|
|
|
121
36
|
/**
|
|
122
|
-
* Fix organization token
|
|
123
|
-
*
|
|
37
|
+
* Fix organization token - deprecated
|
|
38
|
+
* JWT is now the single source of truth, so this is no longer needed.
|
|
39
|
+
* Kept for backwards compatibility but just shows a message.
|
|
124
40
|
*/
|
|
125
41
|
export async function fixOrg() {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
if (!tokenData) {
|
|
129
|
-
console.error(chalk.red('ā Not authenticated. Run: um login'));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!tokenData.selectedOrg) {
|
|
134
|
-
console.error(chalk.yellow('ā ļø No organization selected. Run: um org switch'));
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
console.log(chalk.blue('\nš§ Organization Token Fix\n'));
|
|
139
|
-
console.log(chalk.gray(` Selected org: ${tokenData.selectedOrg.name}`));
|
|
140
|
-
console.log(chalk.gray(` Org ID: ${tokenData.selectedOrg.id}`));
|
|
141
|
-
|
|
142
|
-
// Check if already has org claims
|
|
143
|
-
if (tokenData.decoded?.o?.o_id) {
|
|
144
|
-
console.log(chalk.green('\nā Token already has organization claims'));
|
|
145
|
-
console.log(chalk.gray(` Org in token: ${tokenData.decoded.o.o_id}`));
|
|
146
|
-
if (tokenData.decoded.o.o_id === tokenData.selectedOrg.id) {
|
|
147
|
-
console.log(chalk.green(' ā Org matches selected organization'));
|
|
148
|
-
} else {
|
|
149
|
-
console.log(chalk.yellow(' ā ļø Org mismatch - token has different org than selected'));
|
|
150
|
-
console.log(chalk.gray(' Run: um org switch'));
|
|
151
|
-
}
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
42
|
+
const orgContext = getOrgContext();
|
|
154
43
|
|
|
155
|
-
console.log(chalk.
|
|
44
|
+
console.log(chalk.blue('\nš§ Organization Token Check\n'));
|
|
156
45
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
console.log(chalk.gray(' Refreshing token to obtain session ID...'));
|
|
164
|
-
try {
|
|
165
|
-
const refreshed = await refreshAccessToken(tokenData);
|
|
166
|
-
sessionId = refreshed.decoded?.sid;
|
|
167
|
-
currentToken = refreshed.idToken || refreshed.accessToken;
|
|
168
|
-
|
|
169
|
-
if (sessionId) {
|
|
170
|
-
console.log(chalk.gray(' ā Got session ID from refresh'));
|
|
171
|
-
}
|
|
172
|
-
} catch (error) {
|
|
173
|
-
console.error(chalk.red(` Refresh failed: ${error.message}`));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (!sessionId) {
|
|
178
|
-
console.error(chalk.red('\nā Cannot obtain session ID'));
|
|
179
|
-
console.log(chalk.gray(' The token does not contain a session ID needed for org-scoped tokens.'));
|
|
180
|
-
console.log(chalk.gray(' Run: um login'));
|
|
181
|
-
return;
|
|
46
|
+
if (orgContext) {
|
|
47
|
+
console.log(chalk.green('ā Token has organization claims'));
|
|
48
|
+
console.log(chalk.gray(` Organization: ${orgContext.name}`));
|
|
49
|
+
console.log(chalk.gray(` ID: ${orgContext.id}`));
|
|
50
|
+
} else {
|
|
51
|
+
console.log(chalk.cyan('ā Token is for personal account context'));
|
|
182
52
|
}
|
|
183
53
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
console.log(chalk.cyan('\nš Getting organization-scoped token...'));
|
|
187
|
-
const orgToken = await getOrgScopedToken(
|
|
188
|
-
sessionId,
|
|
189
|
-
tokenData.selectedOrg.id,
|
|
190
|
-
currentToken
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
const decoded = parseJWT(orgToken.jwt);
|
|
194
|
-
saveToken({
|
|
195
|
-
...tokenData,
|
|
196
|
-
idToken: orgToken.jwt,
|
|
197
|
-
decoded: decoded,
|
|
198
|
-
originalSid: sessionId
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
console.log(chalk.green('\nā
Token fixed with organization claims'));
|
|
202
|
-
console.log(chalk.gray(` Org: ${tokenData.selectedOrg.name}`));
|
|
203
|
-
console.log(chalk.gray(` Org ID in token: ${decoded?.o?.o_id || 'N/A'}`));
|
|
204
|
-
} catch (error) {
|
|
205
|
-
console.error(chalk.red(`\nā Failed to get org token: ${error.message}`));
|
|
206
|
-
console.log(chalk.gray('\nYou may need to run: um login'));
|
|
207
|
-
}
|
|
54
|
+
console.log(chalk.gray('\nJWT is now the single source of truth for org context.'));
|
|
55
|
+
console.log(chalk.gray('Use `um org switch` to change organization.'));
|
|
208
56
|
}
|
package/commands/record.js
CHANGED
|
@@ -28,27 +28,30 @@ export async function record(summary, options = {}) {
|
|
|
28
28
|
throw new Error('Invalid project configuration: missing project_id');
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// 3. Build auth headers
|
|
31
|
+
// 3. Build auth headers (JWT is single source of truth)
|
|
32
32
|
const authHeaders = {
|
|
33
33
|
'Authorization': `Bearer ${tokenData.idToken || tokenData.accessToken}`,
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
// Add
|
|
37
|
-
if (tokenData.selectedOrg?.id) {
|
|
38
|
-
authHeaders['X-Org-Id'] = tokenData.selectedOrg.id;
|
|
39
|
-
} else if (tokenData.decoded?.sub) {
|
|
40
|
-
authHeaders['X-Org-Id'] = tokenData.decoded.sub;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Add user ID
|
|
36
|
+
// Add user ID (always present)
|
|
44
37
|
if (tokenData.decoded?.sub) {
|
|
45
38
|
authHeaders['X-User-Id'] = tokenData.decoded.sub;
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
//
|
|
41
|
+
// Only add X-Org-Id if JWT has org claims
|
|
42
|
+
// NO FALLBACK - gateway handles personal context
|
|
43
|
+
if (tokenData.decoded?.o?.o_id) {
|
|
44
|
+
authHeaders['X-Org-Id'] = tokenData.decoded.o.o_id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 4. Build auth context for parameter injection (from JWT claims)
|
|
49
48
|
const authContext = {
|
|
50
49
|
decoded: tokenData.decoded,
|
|
51
|
-
|
|
50
|
+
orgContext: tokenData.decoded?.o ? {
|
|
51
|
+
id: tokenData.decoded.o.o_id,
|
|
52
|
+
name: tokenData.decoded.o.o_name,
|
|
53
|
+
role: tokenData.decoded.o.o_role,
|
|
54
|
+
} : null
|
|
52
55
|
};
|
|
53
56
|
|
|
54
57
|
// 5. Prepare tool arguments
|