@unifiedmemory/cli 1.3.13 → 1.3.15
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/memory-instructions.js +48 -54
- 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
|
@@ -1,82 +1,76 @@
|
|
|
1
1
|
// Unique marker to detect if instructions already present
|
|
2
|
-
export const MEMORY_INSTRUCTIONS_MARKER = '<!--
|
|
2
|
+
export const MEMORY_INSTRUCTIONS_MARKER = '<!-- UNIFIEDMEMORY_INSTRUCTIONS_V3 -->';
|
|
3
3
|
|
|
4
4
|
export function getMemoryInstructions() {
|
|
5
5
|
return `${MEMORY_INSTRUCTIONS_MARKER}
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# STOP - Read Before ANY Code Task
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**MANDATORY**: Call \`search_notes\` BEFORE writing any code. Always. No exceptions.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Pre-Implementation Checklist
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Before ANY coding task, you MUST:
|
|
14
|
+
- [ ] Call \`search_notes\` with relevant keywords
|
|
15
|
+
- [ ] Review results for existing patterns/decisions
|
|
16
|
+
- [ ] Mention what you found (or didn't) in your response
|
|
17
|
+
- [ ] THEN start implementation
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|------|---------|---------------------|
|
|
17
|
-
| \`search_notes\` | **Primary tool** - Search project knowledge base | \`query\` (search text) |
|
|
18
|
-
| \`create_note\` | Record decisions, plans, or completions | \`summary_text\`, \`topic\` |
|
|
19
|
-
| \`list_notes\` | List all notes in current project | None |
|
|
20
|
-
| \`get_note\` | Retrieve a specific note by ID | \`note_id\` |
|
|
21
|
-
| \`list_topics\` | List available topics/categories | None |
|
|
22
|
-
| \`create_topic\` | Create a new topic for organization | \`name\` |
|
|
23
|
-
| \`list_projects\` | List available projects | None |
|
|
19
|
+
---
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
## WRONG
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
\`\`\`
|
|
24
|
+
User: "Create a calculator app"
|
|
25
|
+
Assistant: [immediately writes code] ← NEVER DO THIS
|
|
26
|
+
\`\`\`
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
## CORRECT
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
\`\`\`
|
|
31
|
+
User: "Create a calculator app"
|
|
32
|
+
Assistant: [calls search_notes first]
|
|
33
|
+
Assistant: "No relevant patterns found in UnifiedMemory. Proceeding..."
|
|
34
|
+
Assistant: [writes code]
|
|
35
|
+
\`\`\`
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
---
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
- Implementing any feature (search for existing patterns)
|
|
40
|
-
- Fixing any bug (search for related issues)
|
|
41
|
-
- Answering architecture questions (search for design decisions)
|
|
42
|
-
- Modifying any component (search for existing conventions)
|
|
43
|
-
- ANY coding task, no exceptions
|
|
39
|
+
## When to Search (ALWAYS)
|
|
44
40
|
|
|
45
|
-
|
|
41
|
+
- Before implementing ANY feature ← search first
|
|
42
|
+
- Before fixing ANY bug ← search first
|
|
43
|
+
- Before answering architecture questions ← search first
|
|
44
|
+
- Before modifying ANY component ← search first
|
|
45
|
+
- For "simple" tasks ← search first (context always helps)
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
User: "Add authentication to the API"
|
|
49
|
-
|
|
50
|
-
Step 1: Call search_notes with query "authentication API security"
|
|
51
|
-
Step 2: Review results - found: "Decision: Use JWT with refresh tokens"
|
|
52
|
-
Step 3: Implement following the established pattern
|
|
53
|
-
Step 4: Respond: "Based on prior decisions in UnifiedMemory, I'll use JWT..."
|
|
54
|
-
\`\`\`
|
|
47
|
+
---
|
|
55
48
|
|
|
56
|
-
##
|
|
49
|
+
## MCP Tool Reference
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
| Tool | Purpose | Parameters |
|
|
52
|
+
|------|---------|------------|
|
|
53
|
+
| \`search_notes\` | **Search knowledge base** | \`body: {q: "search text"}\` |
|
|
54
|
+
| \`create_note\` | Record decisions/completions | \`body: {summary_text: "...", topic: "..."}\` |
|
|
55
|
+
| \`list_notes\` | Browse all notes | None |
|
|
56
|
+
| \`get_note\` | Get note by ID | \`body: {note_id: "..."}\` |
|
|
57
|
+
| \`list_topics\` | List categories | None |
|
|
58
|
+
| \`create_topic\` | Create category | \`body: {name: "..."}\` |
|
|
64
59
|
|
|
65
|
-
|
|
60
|
+
Project context is automatic - no extra config needed.
|
|
66
61
|
|
|
67
|
-
|
|
68
|
-
- Implementation decisions and rationale
|
|
69
|
-
- Completed features or fixes
|
|
70
|
-
- Architectural patterns established
|
|
71
|
-
- Gotchas or lessons learned
|
|
62
|
+
---
|
|
72
63
|
|
|
73
|
-
|
|
64
|
+
## After Completing Work
|
|
74
65
|
|
|
75
|
-
|
|
66
|
+
Use \`create_note\` to record:
|
|
67
|
+
- Decisions and rationale (the "why")
|
|
68
|
+
- Patterns established
|
|
69
|
+
- Gotchas or lessons learned
|
|
76
70
|
|
|
77
|
-
|
|
71
|
+
Keep notes under 1000 chars. Focus on "why" not "what".
|
|
78
72
|
|
|
79
73
|
---
|
|
80
|
-
*UnifiedMemory
|
|
74
|
+
*UnifiedMemory - \`search_notes\` FIRST, code SECOND*
|
|
81
75
|
`;
|
|
82
76
|
}
|
package/lib/token-refresh.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { saveToken } from './token-storage.js';
|
|
2
2
|
import { config } from './config.js';
|
|
3
3
|
import { parseJWT } from './jwt-utils.js';
|
|
4
|
-
import { getOrgScopedToken } from './clerk-api.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Check if token has expired
|
|
@@ -23,6 +22,7 @@ export function isTokenExpired(tokenData) {
|
|
|
23
22
|
|
|
24
23
|
/**
|
|
25
24
|
* Refresh access token using refresh token
|
|
25
|
+
* Simple refresh - Clerk preserves org context if it was set
|
|
26
26
|
* @param {Object} tokenData - Current token data
|
|
27
27
|
* @returns {Promise<Object>} - New token data
|
|
28
28
|
*/
|
|
@@ -74,36 +74,22 @@ export async function refreshAccessToken(tokenData) {
|
|
|
74
74
|
|
|
75
75
|
// Parse refreshed JWT
|
|
76
76
|
const refreshedToken = newTokenData.id_token || newTokenData.access_token;
|
|
77
|
-
|
|
78
|
-
let decoded = parseJWT(refreshedToken);
|
|
77
|
+
const decoded = parseJWT(refreshedToken);
|
|
79
78
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const orgToken = await getOrgScopedToken(
|
|
84
|
-
decoded.sid,
|
|
85
|
-
tokenData.selectedOrg.id,
|
|
86
|
-
refreshedToken
|
|
87
|
-
);
|
|
88
|
-
finalIdToken = orgToken.jwt;
|
|
89
|
-
decoded = parseJWT(orgToken.jwt);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
// Log warning but continue with base token
|
|
92
|
-
// The subsequent API call may fail, prompting re-login
|
|
93
|
-
console.error(`⚠️ Could not refresh org-scoped token: ${error.message}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Build updated token object, preserving selectedOrg
|
|
79
|
+
// Build updated token object
|
|
80
|
+
// Note: Clerk preserves org context through refresh automatically
|
|
81
|
+
// We also preserve selectedOrgId/selectedOrgName from login state
|
|
98
82
|
const updatedToken = {
|
|
99
83
|
accessToken: newTokenData.access_token,
|
|
100
|
-
idToken:
|
|
84
|
+
idToken: newTokenData.id_token,
|
|
101
85
|
tokenType: newTokenData.token_type || 'Bearer',
|
|
102
86
|
expiresIn: newTokenData.expires_in,
|
|
103
87
|
receivedAt: Date.now(),
|
|
104
88
|
decoded: decoded,
|
|
105
|
-
selectedOrg: tokenData.selectedOrg, // Preserve organization context
|
|
106
89
|
refresh_token: newTokenData.refresh_token || tokenData.refresh_token,
|
|
90
|
+
sessionId: tokenData.sessionId, // Preserve sessionId through refresh
|
|
91
|
+
selectedOrgId: tokenData.selectedOrgId, // Preserve org selection through refresh
|
|
92
|
+
selectedOrgName: tokenData.selectedOrgName, // Preserve org name through refresh
|
|
107
93
|
};
|
|
108
94
|
|
|
109
95
|
// Save to storage
|
package/lib/token-storage.js
CHANGED
|
@@ -14,12 +14,6 @@ export function saveToken(tokenData) {
|
|
|
14
14
|
// Ensure directory permissions are correct (in case it was created with wrong permissions)
|
|
15
15
|
fs.chmodSync(TOKEN_DIR, 0o700);
|
|
16
16
|
|
|
17
|
-
// Preserve existing organization context if not explicitly overwritten
|
|
18
|
-
const existingData = getToken();
|
|
19
|
-
if (existingData && existingData.selectedOrg && !tokenData.selectedOrg) {
|
|
20
|
-
tokenData.selectedOrg = existingData.selectedOrg;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
17
|
// Write token file with owner-only read/write permissions (0600)
|
|
24
18
|
fs.writeFileSync(TOKEN_FILE, JSON.stringify(tokenData, null, 2));
|
|
25
19
|
// Explicitly set file permissions (writeFileSync mode option doesn't always work with extended attributes)
|
|
@@ -46,31 +40,43 @@ export function clearToken() {
|
|
|
46
40
|
}
|
|
47
41
|
|
|
48
42
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
43
|
+
* Get organization context from JWT claims
|
|
44
|
+
* JWT is the single source of truth for org context
|
|
45
|
+
* @returns {Object|null} Organization context or null if personal context
|
|
51
46
|
*/
|
|
52
|
-
export function
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
export function getOrgContext() {
|
|
48
|
+
const token = getToken();
|
|
49
|
+
|
|
50
|
+
// Try nested 'o' object first (original OAuth token format)
|
|
51
|
+
if (token?.decoded?.o?.o_id) {
|
|
52
|
+
return {
|
|
53
|
+
id: token.decoded.o.o_id,
|
|
54
|
+
name: token.decoded.o.o_name,
|
|
55
|
+
slug: token.decoded.o.o_slug,
|
|
56
|
+
role: token.decoded.o.o_role
|
|
57
|
+
};
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
//
|
|
59
|
-
if (
|
|
60
|
-
|
|
60
|
+
// Fall back to flat org_* claims (org-scoped token from Clerk)
|
|
61
|
+
if (token?.decoded?.org_id) {
|
|
62
|
+
return {
|
|
63
|
+
id: token.decoded.org_id,
|
|
64
|
+
name: token.decoded.org_name,
|
|
65
|
+
slug: token.decoded.org_slug,
|
|
66
|
+
role: token.decoded.org_role
|
|
67
|
+
};
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
// Fall back to selectedOrgId from login state parameter
|
|
71
|
+
// This is used when JWT doesn't have org claims (Clerk setActive limitation)
|
|
72
|
+
if (token?.selectedOrgId) {
|
|
73
|
+
return {
|
|
74
|
+
id: token.selectedOrgId,
|
|
75
|
+
name: token.selectedOrgName || null,
|
|
76
|
+
slug: null,
|
|
77
|
+
role: null
|
|
78
|
+
};
|
|
79
|
+
}
|
|
68
80
|
|
|
69
|
-
|
|
70
|
-
* Get the currently selected organization context
|
|
71
|
-
* @returns {Object|null} Organization data or null if personal context
|
|
72
|
-
*/
|
|
73
|
-
export function getSelectedOrg() {
|
|
74
|
-
const tokenData = getToken();
|
|
75
|
-
return tokenData?.selectedOrg || null;
|
|
81
|
+
return null;
|
|
76
82
|
}
|
package/lib/token-validation.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { getToken
|
|
1
|
+
import { getToken } from './token-storage.js';
|
|
2
2
|
import { isTokenExpired, refreshAccessToken } from './token-refresh.js';
|
|
3
|
-
import { getOrgScopedToken } from './clerk-api.js';
|
|
4
|
-
import { parseJWT } from './jwt-utils.js';
|
|
5
|
-
import { config } from './config.js';
|
|
6
3
|
|
|
7
4
|
/**
|
|
8
5
|
* Load token and refresh if expired
|
|
6
|
+
* JWT is the single source of truth - no recovery logic needed
|
|
9
7
|
* @param {boolean} requireAuth - Whether to throw if not authenticated (default true)
|
|
10
8
|
* @returns {Promise<Object|null>} - Token data or null
|
|
11
9
|
* @throws {Error} - If not authenticated and requireAuth is true, or if refresh fails
|
|
@@ -46,86 +44,5 @@ export async function loadAndRefreshToken(requireAuth = true) {
|
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
// Check if token has org context but lacks org claims in JWT
|
|
50
|
-
// This can happen if getOrgScopedToken() failed during login
|
|
51
|
-
if (tokenData.selectedOrg?.id && !tokenData.decoded?.o) {
|
|
52
|
-
console.error('⚠️ Token missing org claims, attempting to fix...');
|
|
53
|
-
|
|
54
|
-
// Check for sid in decoded token or preserved originalSid from login
|
|
55
|
-
let sessionId = tokenData.decoded?.sid || tokenData.originalSid;
|
|
56
|
-
let currentToken = tokenData.idToken || tokenData.accessToken;
|
|
57
|
-
|
|
58
|
-
// If no sid, try to refresh first to get a new base token with sid
|
|
59
|
-
if (!sessionId && tokenData.refresh_token) {
|
|
60
|
-
console.error(' Refreshing to obtain session ID...');
|
|
61
|
-
try {
|
|
62
|
-
const tokenParams = {
|
|
63
|
-
client_id: config.clerkClientId,
|
|
64
|
-
refresh_token: tokenData.refresh_token,
|
|
65
|
-
grant_type: 'refresh_token',
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
if (config.clerkClientSecret) {
|
|
69
|
-
tokenParams.client_secret = config.clerkClientSecret;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const response = await fetch(`https://${config.clerkDomain}/oauth/token`, {
|
|
73
|
-
method: 'POST',
|
|
74
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
75
|
-
body: new URLSearchParams(tokenParams),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
if (response.ok) {
|
|
79
|
-
const newTokenData = await response.json();
|
|
80
|
-
const refreshedToken = newTokenData.id_token || newTokenData.access_token;
|
|
81
|
-
const refreshedDecoded = parseJWT(refreshedToken);
|
|
82
|
-
|
|
83
|
-
sessionId = refreshedDecoded?.sid;
|
|
84
|
-
currentToken = refreshedToken;
|
|
85
|
-
|
|
86
|
-
// Update tokenData with refreshed values
|
|
87
|
-
tokenData.accessToken = newTokenData.access_token;
|
|
88
|
-
tokenData.idToken = newTokenData.id_token;
|
|
89
|
-
tokenData.refresh_token = newTokenData.refresh_token || tokenData.refresh_token;
|
|
90
|
-
tokenData.decoded = refreshedDecoded;
|
|
91
|
-
|
|
92
|
-
if (sessionId) {
|
|
93
|
-
console.error(' ✓ Obtained session ID');
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
} catch (error) {
|
|
97
|
-
console.error(` Could not refresh token: ${error.message}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Now try to get org-scoped token if we have sessionId
|
|
102
|
-
if (sessionId) {
|
|
103
|
-
try {
|
|
104
|
-
const orgToken = await getOrgScopedToken(
|
|
105
|
-
sessionId,
|
|
106
|
-
tokenData.selectedOrg.id,
|
|
107
|
-
currentToken
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const decoded = parseJWT(orgToken.jwt);
|
|
111
|
-
const updatedToken = {
|
|
112
|
-
...tokenData,
|
|
113
|
-
idToken: orgToken.jwt,
|
|
114
|
-
decoded: decoded,
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
saveToken(updatedToken);
|
|
118
|
-
console.error('✓ Token updated with org claims');
|
|
119
|
-
return updatedToken;
|
|
120
|
-
} catch (error) {
|
|
121
|
-
console.error(`⚠️ Could not get org-scoped token: ${error.message}`);
|
|
122
|
-
// Continue with existing token - API calls may fail
|
|
123
|
-
}
|
|
124
|
-
} else {
|
|
125
|
-
console.error('⚠️ Could not obtain session ID for org-scoped token');
|
|
126
|
-
console.error(' Please run: um login');
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
47
|
return tokenData;
|
|
131
48
|
}
|
package/package.json
CHANGED
|
@@ -221,7 +221,7 @@ describe('mcp-proxy', () => {
|
|
|
221
221
|
|
|
222
222
|
const authContext = {
|
|
223
223
|
decoded: { sub: 'user_123' },
|
|
224
|
-
|
|
224
|
+
orgContext: { id: 'org_456' },
|
|
225
225
|
};
|
|
226
226
|
|
|
227
227
|
const projectContext = {
|
|
@@ -245,7 +245,7 @@ describe('mcp-proxy', () => {
|
|
|
245
245
|
expect(capturedBody.params.arguments.headers['X-Org-Id']).toBe('org_456');
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
-
it('should
|
|
248
|
+
it('should not inject org when no orgContext (personal context)', async () => {
|
|
249
249
|
let capturedBody = null;
|
|
250
250
|
|
|
251
251
|
server.use(
|
|
@@ -263,7 +263,7 @@ describe('mcp-proxy', () => {
|
|
|
263
263
|
|
|
264
264
|
const authContext = {
|
|
265
265
|
decoded: { sub: 'user_123' },
|
|
266
|
-
// No
|
|
266
|
+
// No orgContext - personal context
|
|
267
267
|
};
|
|
268
268
|
|
|
269
269
|
await callRemoteMCPTool(
|
|
@@ -274,9 +274,12 @@ describe('mcp-proxy', () => {
|
|
|
274
274
|
null
|
|
275
275
|
);
|
|
276
276
|
|
|
277
|
-
//
|
|
278
|
-
expect(capturedBody.params.arguments.pathParams.org).
|
|
279
|
-
expect(capturedBody.params.arguments.headers['X-Org-Id']).
|
|
277
|
+
// No org injection for personal context - gateway handles this
|
|
278
|
+
expect(capturedBody.params.arguments.pathParams.org).toBeUndefined();
|
|
279
|
+
expect(capturedBody.params.arguments.headers['X-Org-Id']).toBeUndefined();
|
|
280
|
+
// User should still be injected
|
|
281
|
+
expect(capturedBody.params.arguments.pathParams.user).toBe('user_123');
|
|
282
|
+
expect(capturedBody.params.arguments.headers['X-User-Id']).toBe('user_123');
|
|
280
283
|
});
|
|
281
284
|
|
|
282
285
|
it('should return tool execution result', async () => {
|
|
@@ -44,17 +44,9 @@ vi.mock('../../lib/jwt-utils.js', () => ({
|
|
|
44
44
|
}),
|
|
45
45
|
}));
|
|
46
46
|
|
|
47
|
-
vi.mock('../../lib/clerk-api.js', () => ({
|
|
48
|
-
getOrgScopedToken: vi.fn().mockResolvedValue({
|
|
49
|
-
jwt: 'org_scoped_jwt',
|
|
50
|
-
object: 'token',
|
|
51
|
-
}),
|
|
52
|
-
}));
|
|
53
|
-
|
|
54
47
|
// Import after mocking
|
|
55
48
|
import { isTokenExpired, refreshAccessToken } from '../../lib/token-refresh.js';
|
|
56
49
|
import { saveToken } from '../../lib/token-storage.js';
|
|
57
|
-
import { getOrgScopedToken } from '../../lib/clerk-api.js';
|
|
58
50
|
|
|
59
51
|
describe('isTokenExpired', () => {
|
|
60
52
|
it('returns true for null tokenData', () => {
|
|
@@ -181,7 +173,7 @@ describe('refreshAccessToken', () => {
|
|
|
181
173
|
);
|
|
182
174
|
});
|
|
183
175
|
|
|
184
|
-
it('preserves
|
|
176
|
+
it('preserves selectedOrgId and selectedOrgName in refreshed token', async () => {
|
|
185
177
|
global.fetch = vi.fn().mockResolvedValue({
|
|
186
178
|
ok: true,
|
|
187
179
|
json: () => Promise.resolve({
|
|
@@ -193,60 +185,42 @@ describe('refreshAccessToken', () => {
|
|
|
193
185
|
}),
|
|
194
186
|
});
|
|
195
187
|
|
|
196
|
-
|
|
197
|
-
|
|
188
|
+
await refreshAccessToken({
|
|
189
|
+
refresh_token: 'rt_123',
|
|
190
|
+
selectedOrgId: 'org_456',
|
|
191
|
+
selectedOrgName: 'Test Org',
|
|
192
|
+
});
|
|
198
193
|
|
|
199
194
|
expect(saveToken).toHaveBeenCalledWith(
|
|
200
195
|
expect.objectContaining({
|
|
201
|
-
|
|
196
|
+
selectedOrgId: 'org_456',
|
|
197
|
+
selectedOrgName: 'Test Org',
|
|
202
198
|
})
|
|
203
199
|
);
|
|
204
200
|
});
|
|
205
201
|
|
|
206
|
-
it('
|
|
202
|
+
it('preserves sessionId through refresh', async () => {
|
|
207
203
|
global.fetch = vi.fn().mockResolvedValue({
|
|
208
204
|
ok: true,
|
|
209
205
|
json: () => Promise.resolve({
|
|
210
206
|
access_token: 'new_access_token',
|
|
211
|
-
id_token: '
|
|
207
|
+
id_token: 'new_id_token',
|
|
212
208
|
refresh_token: 'new_refresh_token',
|
|
213
209
|
token_type: 'Bearer',
|
|
214
210
|
expires_in: 3600,
|
|
215
211
|
}),
|
|
216
212
|
});
|
|
217
213
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
expect(getOrgScopedToken).toHaveBeenCalledWith(
|
|
222
|
-
'sess_new_123',
|
|
223
|
-
'org_456',
|
|
224
|
-
'new_id_token_with_sid'
|
|
225
|
-
);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('continues with base token when getOrgScopedToken fails', async () => {
|
|
229
|
-
global.fetch = vi.fn().mockResolvedValue({
|
|
230
|
-
ok: true,
|
|
231
|
-
json: () => Promise.resolve({
|
|
232
|
-
access_token: 'new_access_token',
|
|
233
|
-
id_token: 'new_id_token_with_sid',
|
|
234
|
-
refresh_token: 'new_refresh_token',
|
|
235
|
-
token_type: 'Bearer',
|
|
236
|
-
expires_in: 3600,
|
|
237
|
-
}),
|
|
214
|
+
await refreshAccessToken({
|
|
215
|
+
refresh_token: 'rt_123',
|
|
216
|
+
sessionId: 'sess_123',
|
|
238
217
|
});
|
|
239
218
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// Should not throw - continues with base token
|
|
246
|
-
const result = await refreshAccessToken({ refresh_token: 'rt_123', selectedOrg });
|
|
247
|
-
|
|
248
|
-
expect(result).toBeDefined();
|
|
249
|
-
expect(result.idToken).toBe('new_id_token_with_sid'); // Falls back to base token
|
|
219
|
+
expect(saveToken).toHaveBeenCalledWith(
|
|
220
|
+
expect.objectContaining({
|
|
221
|
+
sessionId: 'sess_123',
|
|
222
|
+
})
|
|
223
|
+
);
|
|
250
224
|
});
|
|
251
225
|
|
|
252
226
|
it('throws error when OAuth refresh fails with 400', async () => {
|
|
@@ -132,7 +132,6 @@ describe('token-storage module behavior', () => {
|
|
|
132
132
|
expect(typeof ts.saveToken).toBe('function');
|
|
133
133
|
expect(typeof ts.getToken).toBe('function');
|
|
134
134
|
expect(typeof ts.clearToken).toBe('function');
|
|
135
|
-
expect(typeof ts.
|
|
136
|
-
expect(typeof ts.getSelectedOrg).toBe('function');
|
|
135
|
+
expect(typeof ts.getOrgContext).toBe('function');
|
|
137
136
|
});
|
|
138
137
|
});
|