@unifiedmemory/cli 1.3.12 ā 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 +18 -0
- package/README.md +28 -0
- package/commands/debug.js +67 -0
- package/commands/init.js +17 -8
- package/commands/login.js +138 -101
- 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 +21 -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
|
@@ -37,6 +37,24 @@
|
|
|
37
37
|
# REDIRECT_URI=http://localhost:3333/callback
|
|
38
38
|
# PORT=3333
|
|
39
39
|
|
|
40
|
+
# ============================================
|
|
41
|
+
# OAuth Success Page Link
|
|
42
|
+
# ============================================
|
|
43
|
+
# Optional website link shown on the OAuth success page
|
|
44
|
+
# Users can visit this URL to manage their account or subscription
|
|
45
|
+
# Default: https://unifiedmemory.ai/oauth/callback
|
|
46
|
+
# OAUTH_SUCCESS_URL=https://unifiedmemory.ai/oauth/callback
|
|
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
|
+
|
|
40
58
|
# ============================================
|
|
41
59
|
# Clerk Client Secret (Optional)
|
|
42
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>
|
|
@@ -143,24 +155,111 @@ export async function login() {
|
|
|
143
155
|
// Debug: Log the token response structure
|
|
144
156
|
console.log(chalk.gray('\nToken response keys:'), Object.keys(tokenData));
|
|
145
157
|
|
|
146
|
-
// Send success response to browser
|
|
158
|
+
// Send success response to browser
|
|
147
159
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
148
160
|
res.end(`
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<
|
|
153
|
-
<
|
|
154
|
-
<
|
|
161
|
+
<!DOCTYPE html>
|
|
162
|
+
<html lang="en">
|
|
163
|
+
<head>
|
|
164
|
+
<meta charset="UTF-8">
|
|
165
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
166
|
+
<title>Login Successful</title>
|
|
167
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
168
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
169
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
170
|
+
<style>
|
|
171
|
+
* {
|
|
172
|
+
margin: 0;
|
|
173
|
+
padding: 0;
|
|
174
|
+
box-sizing: border-box;
|
|
175
|
+
}
|
|
176
|
+
body {
|
|
177
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
178
|
+
background: hsl(220, 30%, 8%);
|
|
179
|
+
color: hsl(0, 0%, 100%);
|
|
180
|
+
min-height: 100vh;
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
padding: 2rem;
|
|
185
|
+
}
|
|
186
|
+
.card {
|
|
187
|
+
background: hsl(220, 25%, 12%);
|
|
188
|
+
border: 1px solid hsl(220, 20%, 20%);
|
|
189
|
+
border-radius: 0.5rem;
|
|
190
|
+
padding: 2rem;
|
|
191
|
+
text-align: center;
|
|
192
|
+
max-width: 32rem;
|
|
193
|
+
width: 100%;
|
|
194
|
+
}
|
|
195
|
+
.logo {
|
|
196
|
+
width: 64px;
|
|
197
|
+
height: 64px;
|
|
198
|
+
margin: 0 auto 1.5rem;
|
|
199
|
+
display: block;
|
|
200
|
+
}
|
|
201
|
+
h1 {
|
|
202
|
+
font-size: 1.875rem;
|
|
203
|
+
font-weight: 700;
|
|
204
|
+
margin-bottom: 0.5rem;
|
|
205
|
+
color: hsl(0, 0%, 100%);
|
|
206
|
+
}
|
|
207
|
+
.description {
|
|
208
|
+
font-size: 1.125rem;
|
|
209
|
+
color: hsl(0, 0%, 65%);
|
|
210
|
+
margin-bottom: 1.5rem;
|
|
211
|
+
}
|
|
212
|
+
.message {
|
|
213
|
+
color: hsl(0, 0%, 65%);
|
|
214
|
+
line-height: 1.6;
|
|
215
|
+
}
|
|
216
|
+
.optional-link {
|
|
217
|
+
margin-top: 2rem;
|
|
218
|
+
padding-top: 1.5rem;
|
|
219
|
+
border-top: 1px solid hsl(220, 20%, 20%);
|
|
220
|
+
font-size: 0.8125rem;
|
|
221
|
+
color: hsl(0, 0%, 50%);
|
|
222
|
+
line-height: 1.5;
|
|
223
|
+
}
|
|
224
|
+
.optional-link a {
|
|
225
|
+
color: hsl(0, 0%, 55%);
|
|
226
|
+
text-decoration: none;
|
|
227
|
+
border-bottom: 1px solid transparent;
|
|
228
|
+
transition: border-color 0.2s;
|
|
229
|
+
}
|
|
230
|
+
.optional-link a:hover {
|
|
231
|
+
border-bottom-color: hsl(0, 0%, 55%);
|
|
232
|
+
}
|
|
233
|
+
</style>
|
|
234
|
+
</head>
|
|
235
|
+
<body>
|
|
236
|
+
<div class="card">
|
|
237
|
+
<img src="https://unifiedmemory.ai/images/theme/axolotl-logo.png" alt="UnifiedMemory.ai Logo" class="logo" />
|
|
238
|
+
<h1>Login Successful</h1>
|
|
239
|
+
<p class="description">Your authentication is complete.</p>
|
|
240
|
+
<p class="message">You may now close this tab and return to the terminal to continue using the CLI.</p>
|
|
241
|
+
<div class="optional-link">
|
|
242
|
+
You can manage your account or subscription on the web at<br>
|
|
243
|
+
<a href="https://unifiedmemory.ai/account">unifiedmemory.ai</a>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
<script>
|
|
247
|
+
// Auto-close window after 10 seconds
|
|
248
|
+
setTimeout(() => {
|
|
249
|
+
window.close();
|
|
250
|
+
}, 10000);
|
|
251
|
+
</script>
|
|
155
252
|
</body>
|
|
156
253
|
</html>
|
|
157
254
|
`);
|
|
158
255
|
|
|
159
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
|
|
160
259
|
const tokenToParse = tokenData.id_token || tokenData.access_token;
|
|
161
260
|
const decoded = parseJWT(tokenToParse);
|
|
162
261
|
|
|
163
|
-
// Save token (
|
|
262
|
+
// Save token with selected org from state (since JWT doesn't have org claims)
|
|
164
263
|
saveToken({
|
|
165
264
|
accessToken: tokenData.access_token,
|
|
166
265
|
idToken: tokenData.id_token,
|
|
@@ -168,7 +267,10 @@ export async function login() {
|
|
|
168
267
|
tokenType: tokenData.token_type || 'Bearer',
|
|
169
268
|
expiresIn: tokenData.expires_in,
|
|
170
269
|
receivedAt: Date.now(),
|
|
171
|
-
decoded: decoded
|
|
270
|
+
decoded: decoded,
|
|
271
|
+
sessionId: decoded?.sid,
|
|
272
|
+
selectedOrgId: selectedOrgId, // From state parameter
|
|
273
|
+
selectedOrgName: selectedOrgName, // From state parameter
|
|
172
274
|
});
|
|
173
275
|
|
|
174
276
|
console.log(chalk.green('\nā
Authentication successful!'));
|
|
@@ -184,95 +286,30 @@ export async function login() {
|
|
|
184
286
|
}
|
|
185
287
|
}
|
|
186
288
|
|
|
187
|
-
// Close server
|
|
188
|
-
server.close(
|
|
289
|
+
// Close server and display org context
|
|
290
|
+
server.close(() => {
|
|
189
291
|
console.log(chalk.gray('ā Callback server closed'));
|
|
190
292
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
console.log(chalk.blue('\nš Checking for organizations...'));
|
|
205
|
-
const selectedOrg = await promptOrganizationSelection(memberships);
|
|
206
|
-
displayOrganizationSelection(selectedOrg);
|
|
207
|
-
|
|
208
|
-
if (selectedOrg) {
|
|
209
|
-
// Get org-scoped JWT from Clerk
|
|
210
|
-
try {
|
|
211
|
-
console.log(chalk.cyan('\nš Getting organization-scoped token...'));
|
|
212
|
-
|
|
213
|
-
const sessionId = decoded.sid;
|
|
214
|
-
if (!sessionId) {
|
|
215
|
-
throw new Error('No session ID found in token');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const orgToken = await getOrgScopedToken(
|
|
219
|
-
sessionId,
|
|
220
|
-
selectedOrg.id,
|
|
221
|
-
tokenData.id_token
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
// Update saved token with org-scoped version
|
|
225
|
-
// Preserve originalSid for recovery purposes (org-scoped token won't have sid)
|
|
226
|
-
saveToken({
|
|
227
|
-
accessToken: tokenData.access_token,
|
|
228
|
-
idToken: orgToken.jwt,
|
|
229
|
-
refresh_token: tokenData.refresh_token,
|
|
230
|
-
tokenType: 'Bearer',
|
|
231
|
-
expiresIn: tokenData.expires_in,
|
|
232
|
-
receivedAt: Date.now(),
|
|
233
|
-
decoded: parseJWT(orgToken.jwt),
|
|
234
|
-
selectedOrg: selectedOrg,
|
|
235
|
-
originalSid: decoded.sid // Preserve session ID from original OAuth token
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
console.log(chalk.green(`\nā
Using organization context: ${chalk.bold(selectedOrg.name)}`));
|
|
239
|
-
console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
|
|
240
|
-
console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
|
|
241
|
-
console.log(chalk.gray(' ā Token updated with organization context'));
|
|
242
|
-
} catch (error) {
|
|
243
|
-
console.error(chalk.yellow('\nā ļø Failed to get org-scoped token:'), error.message);
|
|
244
|
-
console.log(chalk.gray(' Continuing with original token (may have limited org access)'));
|
|
245
|
-
|
|
246
|
-
// Save token with selectedOrg AND originalSid for recovery
|
|
247
|
-
// This allows token-validation.js to retry getting org-scoped token later
|
|
248
|
-
saveToken({
|
|
249
|
-
accessToken: tokenData.access_token,
|
|
250
|
-
idToken: tokenData.id_token,
|
|
251
|
-
refresh_token: tokenData.refresh_token,
|
|
252
|
-
tokenType: 'Bearer',
|
|
253
|
-
expiresIn: tokenData.expires_in,
|
|
254
|
-
receivedAt: Date.now(),
|
|
255
|
-
decoded: decoded,
|
|
256
|
-
selectedOrg: selectedOrg,
|
|
257
|
-
originalSid: decoded.sid // Preserve session ID for recovery
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
console.log(chalk.green(`\nā
Using organization context: ${chalk.bold(selectedOrg.name)}`));
|
|
261
|
-
console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
|
|
262
|
-
console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
|
|
263
|
-
console.log(chalk.yellow(' ā ļø Token lacks org claims - recovery will be attempted on next use'));
|
|
264
|
-
}
|
|
265
|
-
} else {
|
|
266
|
-
console.log(chalk.green('\nā
Using personal account context'));
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
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}`));
|
|
270
306
|
}
|
|
271
|
-
}
|
|
272
|
-
console.log(chalk.
|
|
273
|
-
console.log(chalk.gray(`Error: ${error.message}`));
|
|
307
|
+
} else {
|
|
308
|
+
console.log(chalk.green('\nā
Using personal account context'));
|
|
274
309
|
}
|
|
275
310
|
|
|
311
|
+
console.log(chalk.gray('\nYou can switch organizations anytime with: um org switch'));
|
|
312
|
+
|
|
276
313
|
resolve(tokenData);
|
|
277
314
|
});
|
|
278
315
|
} catch (error) {
|