@topolo/sdk 0.7.0 → 0.8.0
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/README.md +11 -3
- package/dist/index.cjs +187 -53
- package/dist/index.d.cts +52 -8
- package/dist/index.d.ts +52 -8
- package/dist/index.js +186 -53
- package/package.json +1 -2
- package/src/applications.generated.ts +1 -0
- package/src/client.test.ts +294 -31
- package/src/client.ts +280 -59
- package/src/errors.ts +7 -0
- package/src/index.ts +1 -0
- package/src/modules/crm.test.ts +1 -3
- package/src/modules/p2p.test.ts +1 -3
- package/src/node/credential-store.test.ts +337 -0
- package/src/node/credential-store.ts +19 -5
- package/dist/node/credential-store.cjs +0 -471
- package/dist/node/credential-store.d.cts +0 -108
- package/dist/node/credential-store.d.ts +0 -108
- package/dist/node/credential-store.js +0 -427
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { readConfig, refreshCredentialIfNeeded, writeConfig } from './credential-store.js';
|
|
6
|
+
import {
|
|
7
|
+
activateContext,
|
|
8
|
+
clearProfileCredential,
|
|
9
|
+
configPath,
|
|
10
|
+
contextKey,
|
|
11
|
+
credentialSourceFromConfig,
|
|
12
|
+
envVarPrefix,
|
|
13
|
+
normalizeEnvironment,
|
|
14
|
+
profileForEnvironment,
|
|
15
|
+
resolveAgentName,
|
|
16
|
+
resolveCredential,
|
|
17
|
+
resolvedProfileConfig,
|
|
18
|
+
resolveServiceUrls,
|
|
19
|
+
selectedEnvironment,
|
|
20
|
+
writeContextCredential,
|
|
21
|
+
writeProfileConfig,
|
|
22
|
+
} from './credential-store.js';
|
|
23
|
+
|
|
24
|
+
const originalEnv = { ...process.env };
|
|
25
|
+
const originalFetch = globalThis.fetch;
|
|
26
|
+
let tempDirectory: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
tempDirectory = await mkdtemp(join(tmpdir(), 'topolo-sdk-credential-'));
|
|
30
|
+
process.env = { ...originalEnv, TOPOLO_CONFIG_PATH: join(tempDirectory, 'config.json') };
|
|
31
|
+
delete process.env.TOPOLO_API_KEY;
|
|
32
|
+
delete process.env.TOPOLO_ACCESS_TOKEN;
|
|
33
|
+
delete process.env.TOPOLO_ENV;
|
|
34
|
+
delete process.env.TOPOLO_AGENT_NAME;
|
|
35
|
+
delete process.env.TOPOLO_PRODUCTION_API_KEY;
|
|
36
|
+
delete process.env.TOPOLO_PRODUCTION_ACCESS_TOKEN;
|
|
37
|
+
delete process.env.TOPOLO_STAGING_API_KEY;
|
|
38
|
+
delete process.env.TOPOLO_STAGING_ACCESS_TOKEN;
|
|
39
|
+
for (const key of Object.keys(process.env)) {
|
|
40
|
+
if (key.startsWith('TOPOLO_SERVICE_URL_') || key.startsWith('TOPOLO_STAGING_SERVICE_URL_')) {
|
|
41
|
+
delete process.env[key];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('credential configuration primitives', () => {
|
|
47
|
+
it('normalizes environment aliases and rejects unknown values', () => {
|
|
48
|
+
expect(normalizeEnvironment(undefined)).toBe('production');
|
|
49
|
+
expect(normalizeEnvironment(' ')).toBe('production');
|
|
50
|
+
expect(normalizeEnvironment('PROD')).toBe('production');
|
|
51
|
+
expect(normalizeEnvironment('stage')).toBe('staging');
|
|
52
|
+
expect(normalizeEnvironment('stg')).toBe('staging');
|
|
53
|
+
expect(normalizeEnvironment('dev')).toBe('development');
|
|
54
|
+
expect(normalizeEnvironment('localhost')).toBe('local');
|
|
55
|
+
expect(() => normalizeEnvironment('preview')).toThrow(/Unknown Topolo environment/);
|
|
56
|
+
|
|
57
|
+
process.env.TOPOLO_ENV = 'staging';
|
|
58
|
+
expect(selectedEnvironment()).toBe('staging');
|
|
59
|
+
expect(envVarPrefix('production')).toBe('TOPOLO_PRODUCTION');
|
|
60
|
+
expect(envVarPrefix('staging')).toBe('TOPOLO_STAGING');
|
|
61
|
+
expect(envVarPrefix('development')).toBe('TOPOLO_DEVELOPMENT');
|
|
62
|
+
expect(envVarPrefix('local')).toBe('TOPOLO_LOCAL');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('uses the explicit config path and safely reads missing or malformed config', async () => {
|
|
66
|
+
expect(configPath()).toBe(join(tempDirectory, 'config.json'));
|
|
67
|
+
expect(await readConfig()).toEqual({});
|
|
68
|
+
await import('node:fs/promises').then(({ writeFile }) => writeFile(configPath(), '{bad json'));
|
|
69
|
+
expect(await readConfig()).toEqual({});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('merges legacy production fields with profiles and writes environment profiles', () => {
|
|
73
|
+
const config = {
|
|
74
|
+
credential: { kind: 'api_key' as const, apiKey: 'legacy' },
|
|
75
|
+
oauthClientId: 'legacy-client',
|
|
76
|
+
serviceUrls: { auth: 'https://legacy-auth', developers: 'https://legacy-dev' },
|
|
77
|
+
agentName: 'legacy-agent',
|
|
78
|
+
profiles: {
|
|
79
|
+
production: {
|
|
80
|
+
serviceUrls: { auth: 'https://profile-auth' },
|
|
81
|
+
agentName: 'profile-agent',
|
|
82
|
+
},
|
|
83
|
+
staging: { agentName: 'staging-agent' },
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
expect(profileForEnvironment(config, 'production')).toMatchObject({
|
|
87
|
+
credential: config.credential,
|
|
88
|
+
oauthClientId: 'legacy-client',
|
|
89
|
+
serviceUrls: { auth: 'https://profile-auth', developers: 'https://legacy-dev' },
|
|
90
|
+
agentName: 'profile-agent',
|
|
91
|
+
});
|
|
92
|
+
expect(profileForEnvironment(config, 'staging')).toEqual({ agentName: 'staging-agent' });
|
|
93
|
+
expect(writeProfileConfig(config, 'staging', { oauthClientId: 'new-client' }).profiles?.staging)
|
|
94
|
+
.toEqual({ agentName: 'staging-agent', oauthClientId: 'new-client' });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('stores, activates, and clears personal and organization contexts', () => {
|
|
98
|
+
expect(contextKey('personal')).toBe('personal');
|
|
99
|
+
expect(contextKey('organization', 'org_1')).toBe('org:org_1');
|
|
100
|
+
expect(contextKey('organization')).toBe('personal');
|
|
101
|
+
|
|
102
|
+
const organization = {
|
|
103
|
+
contextType: 'organization' as const,
|
|
104
|
+
organizationId: 'org_1',
|
|
105
|
+
oauthClientId: 'context-client',
|
|
106
|
+
credential: { kind: 'access_token' as const, accessToken: 'org-token' },
|
|
107
|
+
};
|
|
108
|
+
const stored = writeContextCredential({}, 'staging', organization);
|
|
109
|
+
expect(stored.profiles?.staging).toMatchObject({
|
|
110
|
+
activeContext: 'org:org_1',
|
|
111
|
+
credential: organization.credential,
|
|
112
|
+
oauthClientId: 'context-client',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const inactive = writeContextCredential(stored, 'staging', {
|
|
116
|
+
contextType: 'personal',
|
|
117
|
+
credential: { kind: 'access_token', accessToken: 'personal-token' },
|
|
118
|
+
}, false);
|
|
119
|
+
expect(inactive.profiles?.staging?.activeContext).toBe('org:org_1');
|
|
120
|
+
const activated = activateContext(inactive, 'staging', 'personal');
|
|
121
|
+
expect(activated.profiles?.staging?.credential).toMatchObject({ accessToken: 'personal-token' });
|
|
122
|
+
expect(activateContext(activated, 'staging', 'missing')).toBe(activated);
|
|
123
|
+
|
|
124
|
+
const production = clearProfileCredential({
|
|
125
|
+
credential: { kind: 'api_key', apiKey: 'legacy' },
|
|
126
|
+
oauthClientId: 'client',
|
|
127
|
+
profiles: { production: { credential: organization.credential, oauthClientId: 'profile' } },
|
|
128
|
+
}, 'production');
|
|
129
|
+
expect(production.credential).toBeUndefined();
|
|
130
|
+
expect(production.oauthClientId).toBeUndefined();
|
|
131
|
+
expect(production.profiles?.production?.credential).toBeUndefined();
|
|
132
|
+
expect(clearProfileCredential(stored, 'staging').profiles?.staging?.credential).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('credential and service resolution', () => {
|
|
137
|
+
it('reports credential source precedence and resolves environment credentials', async () => {
|
|
138
|
+
const config = { credential: { kind: 'api_key' as const, apiKey: 'stored' } };
|
|
139
|
+
expect(credentialSourceFromConfig(config, 'production')).toBe('config-file');
|
|
140
|
+
process.env.TOPOLO_ACCESS_TOKEN = 'global-access';
|
|
141
|
+
expect(credentialSourceFromConfig(config, 'production')).toBe('env:TOPOLO_ACCESS_TOKEN');
|
|
142
|
+
expect(await resolveCredential('production')).toEqual({ kind: 'access_token', accessToken: 'global-access' });
|
|
143
|
+
process.env.TOPOLO_API_KEY = 'global-key';
|
|
144
|
+
expect(credentialSourceFromConfig(config, 'production')).toBe('env:TOPOLO_API_KEY');
|
|
145
|
+
expect(await resolveCredential('production')).toEqual({ kind: 'api_key', apiKey: 'global-key' });
|
|
146
|
+
process.env.TOPOLO_PRODUCTION_ACCESS_TOKEN = 'scoped-access';
|
|
147
|
+
expect(credentialSourceFromConfig(config, 'production')).toBe('env:TOPOLO_PRODUCTION_ACCESS_TOKEN');
|
|
148
|
+
process.env.TOPOLO_PRODUCTION_API_KEY = 'scoped-key';
|
|
149
|
+
expect(credentialSourceFromConfig(config, 'production')).toBe('env:TOPOLO_PRODUCTION_API_KEY');
|
|
150
|
+
expect(await resolveCredential('production')).toEqual({ kind: 'api_key', apiKey: 'scoped-key' });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('falls back to stored credentials or none', async () => {
|
|
154
|
+
expect(credentialSourceFromConfig({}, 'staging')).toBe('none');
|
|
155
|
+
expect(await resolveCredential('staging')).toBeNull();
|
|
156
|
+
await writeConfig({ profiles: { staging: { credential: { kind: 'api_key', apiKey: 'stored' } } } });
|
|
157
|
+
expect(await resolveCredential('staging')).toEqual({ kind: 'api_key', apiKey: 'stored' });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('merges defaults, config, scoped env, and global env in precedence order', () => {
|
|
161
|
+
process.env.TOPOLO_STAGING_SERVICE_URL_AUTH = 'https://scoped-auth';
|
|
162
|
+
process.env.TOPOLO_SERVICE_URL_AUTH = 'https://global-auth';
|
|
163
|
+
process.env.TOPOLO_SERVICE_URL_CUSTOM = 'https://custom';
|
|
164
|
+
expect(resolveServiceUrls({ auth: 'https://config-auth' }, 'staging')).toMatchObject({
|
|
165
|
+
auth: 'https://global-auth',
|
|
166
|
+
developers: 'https://developers.stg.topolo.us',
|
|
167
|
+
seed: 'https://seed.stg.topolo.us',
|
|
168
|
+
custom: 'https://custom',
|
|
169
|
+
});
|
|
170
|
+
delete process.env.TOPOLO_STAGING_SERVICE_URL_AUTH;
|
|
171
|
+
delete process.env.TOPOLO_SERVICE_URL_AUTH;
|
|
172
|
+
delete process.env.TOPOLO_SERVICE_URL_CUSTOM;
|
|
173
|
+
expect(resolveServiceUrls(undefined, 'local')).toBeUndefined();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('resolves agent and complete profile settings', () => {
|
|
177
|
+
expect(resolveAgentName('config-agent')).toBe('config-agent');
|
|
178
|
+
process.env.TOPOLO_AGENT_NAME = 'env-agent';
|
|
179
|
+
expect(resolveAgentName('config-agent')).toBe('env-agent');
|
|
180
|
+
const resolved = resolvedProfileConfig({
|
|
181
|
+
profiles: {
|
|
182
|
+
staging: {
|
|
183
|
+
oauthClientId: 'client',
|
|
184
|
+
credential: { kind: 'api_key', apiKey: 'stored' },
|
|
185
|
+
agentName: 'profile-agent',
|
|
186
|
+
serviceUrls: { auth: 'https://custom-auth' },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
}, 'staging');
|
|
190
|
+
expect(resolved).toMatchObject({
|
|
191
|
+
oauthClientId: 'client',
|
|
192
|
+
credential: { kind: 'api_key', apiKey: 'stored' },
|
|
193
|
+
agentName: 'profile-agent',
|
|
194
|
+
serviceUrls: { auth: 'https://custom-auth' },
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
afterEach(async () => {
|
|
200
|
+
process.env = { ...originalEnv };
|
|
201
|
+
globalThis.fetch = originalFetch;
|
|
202
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('refreshCredentialIfNeeded force mode', () => {
|
|
206
|
+
it('refreshes a stored token after a 401 even when its expiry is still in the future', async () => {
|
|
207
|
+
await writeConfig({
|
|
208
|
+
credential: {
|
|
209
|
+
kind: 'access_token',
|
|
210
|
+
accessToken: 'rejected',
|
|
211
|
+
refreshToken: 'refresh',
|
|
212
|
+
expiresAt: Math.floor(Date.now() / 1000) + 10_000,
|
|
213
|
+
},
|
|
214
|
+
oauthClientId: 'topolo-cli',
|
|
215
|
+
});
|
|
216
|
+
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
|
|
217
|
+
access_token: 'fresh',
|
|
218
|
+
token_type: 'Bearer',
|
|
219
|
+
expires_in: 3600,
|
|
220
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } })) as unknown as typeof fetch;
|
|
221
|
+
|
|
222
|
+
const credential = await refreshCredentialIfNeeded('production', { force: true });
|
|
223
|
+
|
|
224
|
+
expect(credential).toMatchObject({ accessToken: 'fresh', refreshToken: 'refresh' });
|
|
225
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('rotates the active context credential as well as the profile pointer', async () => {
|
|
229
|
+
await writeConfig({
|
|
230
|
+
profiles: {
|
|
231
|
+
staging: {
|
|
232
|
+
activeContext: 'org:org_a',
|
|
233
|
+
oauthClientId: 'topolo-cli',
|
|
234
|
+
credential: {
|
|
235
|
+
kind: 'access_token',
|
|
236
|
+
accessToken: 'old-a',
|
|
237
|
+
refreshToken: 'refresh-a',
|
|
238
|
+
expiresAt: 1,
|
|
239
|
+
},
|
|
240
|
+
contexts: {
|
|
241
|
+
'org:org_a': {
|
|
242
|
+
contextType: 'organization',
|
|
243
|
+
organizationId: 'org_a',
|
|
244
|
+
credential: {
|
|
245
|
+
kind: 'access_token',
|
|
246
|
+
accessToken: 'old-a',
|
|
247
|
+
refreshToken: 'refresh-a',
|
|
248
|
+
expiresAt: 1,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
'org:org_b': {
|
|
252
|
+
contextType: 'organization',
|
|
253
|
+
organizationId: 'org_b',
|
|
254
|
+
credential: {
|
|
255
|
+
kind: 'access_token',
|
|
256
|
+
accessToken: 'untouched-b',
|
|
257
|
+
refreshToken: 'refresh-b',
|
|
258
|
+
expiresAt: 2,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
|
|
266
|
+
access_token: 'fresh-a',
|
|
267
|
+
token_type: 'Bearer',
|
|
268
|
+
expires_in: 3600,
|
|
269
|
+
refresh_token: 'rotated-a',
|
|
270
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } })) as unknown as typeof fetch;
|
|
271
|
+
|
|
272
|
+
await refreshCredentialIfNeeded('staging');
|
|
273
|
+
|
|
274
|
+
const profile = (await readConfig()).profiles?.staging;
|
|
275
|
+
expect(profile?.credential).toMatchObject({ accessToken: 'fresh-a', refreshToken: 'rotated-a' });
|
|
276
|
+
expect(profile?.contexts?.['org:org_a']?.credential).toMatchObject({
|
|
277
|
+
accessToken: 'fresh-a',
|
|
278
|
+
refreshToken: 'rotated-a',
|
|
279
|
+
});
|
|
280
|
+
expect(profile?.contexts?.['org:org_b']?.credential).toMatchObject({
|
|
281
|
+
accessToken: 'untouched-b',
|
|
282
|
+
refreshToken: 'refresh-b',
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('uses environment credentials and skips ineligible or unexpired stored credentials', async () => {
|
|
287
|
+
process.env.TOPOLO_STAGING_API_KEY = 'env-key';
|
|
288
|
+
expect(await refreshCredentialIfNeeded('staging')).toEqual({ kind: 'api_key', apiKey: 'env-key' });
|
|
289
|
+
delete process.env.TOPOLO_STAGING_API_KEY;
|
|
290
|
+
expect(await refreshCredentialIfNeeded('staging')).toBeNull();
|
|
291
|
+
|
|
292
|
+
await writeConfig({ profiles: { staging: { credential: { kind: 'api_key', apiKey: 'stored' } } } });
|
|
293
|
+
expect(await refreshCredentialIfNeeded('staging')).toEqual({ kind: 'api_key', apiKey: 'stored' });
|
|
294
|
+
|
|
295
|
+
const access = { kind: 'access_token' as const, accessToken: 'fresh', expiresAt: Math.floor(Date.now() / 1000) + 10_000 };
|
|
296
|
+
await writeConfig({ profiles: { staging: { credential: access, oauthClientId: 'client' } } });
|
|
297
|
+
expect(await refreshCredentialIfNeeded('staging')).toEqual(access);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('returns the existing credential when refresh inputs are incomplete or exchange fails', async () => {
|
|
301
|
+
const withoutRefresh = { kind: 'access_token' as const, accessToken: 'token', expiresAt: 1 };
|
|
302
|
+
await writeConfig({ profiles: { staging: { credential: withoutRefresh, oauthClientId: 'client' } } });
|
|
303
|
+
expect(await refreshCredentialIfNeeded('staging')).toEqual(withoutRefresh);
|
|
304
|
+
|
|
305
|
+
const expired = { ...withoutRefresh, refreshToken: 'refresh' };
|
|
306
|
+
await writeConfig({ profiles: { staging: { credential: expired, oauthClientId: 'client' } } });
|
|
307
|
+
globalThis.fetch = vi.fn(async () => new Response('unavailable', { status: 503 })) as unknown as typeof fetch;
|
|
308
|
+
expect(await refreshCredentialIfNeeded('staging')).toEqual(expired);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('persists refreshed credentials into an explicit production profile', async () => {
|
|
312
|
+
await writeConfig({
|
|
313
|
+
credential: { kind: 'api_key', apiKey: 'legacy' },
|
|
314
|
+
profiles: {
|
|
315
|
+
production: {
|
|
316
|
+
oauthClientId: 'client',
|
|
317
|
+
credential: { kind: 'access_token', accessToken: 'old', refreshToken: 'refresh', expiresAt: 1 },
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
|
|
322
|
+
access_token: 'profile-fresh',
|
|
323
|
+
token_type: 'Bearer',
|
|
324
|
+
expires_in: 1200,
|
|
325
|
+
refresh_token: 'profile-refresh',
|
|
326
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } })) as unknown as typeof fetch;
|
|
327
|
+
|
|
328
|
+
await refreshCredentialIfNeeded('production');
|
|
329
|
+
|
|
330
|
+
const config = await readConfig();
|
|
331
|
+
expect(config.credential).toEqual({ kind: 'api_key', apiKey: 'legacy' });
|
|
332
|
+
expect(config.profiles?.production?.credential).toMatchObject({
|
|
333
|
+
accessToken: 'profile-fresh',
|
|
334
|
+
refreshToken: 'profile-refresh',
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -287,6 +287,7 @@ const REFRESH_SKEW_SECONDS = 60;
|
|
|
287
287
|
|
|
288
288
|
export async function refreshCredentialIfNeeded(
|
|
289
289
|
environment: TopoloEnvironment = selectedEnvironment(),
|
|
290
|
+
options: { force?: boolean } = {},
|
|
290
291
|
): Promise<TopoloCredential | null> {
|
|
291
292
|
const prefix = envVarPrefix(environment);
|
|
292
293
|
if (
|
|
@@ -305,7 +306,7 @@ export async function refreshCredentialIfNeeded(
|
|
|
305
306
|
if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
|
|
306
307
|
|
|
307
308
|
const now = Math.floor(Date.now() / 1000);
|
|
308
|
-
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
309
|
+
if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
309
310
|
|
|
310
311
|
try {
|
|
311
312
|
const oauth = new TopoloOAuth({
|
|
@@ -325,11 +326,24 @@ export async function refreshCredentialIfNeeded(
|
|
|
325
326
|
: { refreshToken: cred.refreshToken }),
|
|
326
327
|
expiresAt: Math.floor(Date.now() / 1000) + tokens.expires_in,
|
|
327
328
|
};
|
|
328
|
-
|
|
329
|
-
|
|
329
|
+
const activeContext = profile.activeContext;
|
|
330
|
+
const activeEntry = activeContext ? profile.contexts?.[activeContext] : undefined;
|
|
331
|
+
const updated = activeContext && activeEntry
|
|
332
|
+
? writeProfileConfig(config, environment, {
|
|
333
|
+
credential: refreshed,
|
|
334
|
+
contexts: {
|
|
335
|
+
...(profile.contexts ?? {}),
|
|
336
|
+
[activeContext]: {
|
|
337
|
+
...activeEntry,
|
|
338
|
+
credential: refreshed,
|
|
339
|
+
oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
: environment === 'production' && !config.profiles?.production
|
|
330
344
|
? { ...config, credential: refreshed }
|
|
331
|
-
: writeProfileConfig(config, environment, { credential: refreshed })
|
|
332
|
-
);
|
|
345
|
+
: writeProfileConfig(config, environment, { credential: refreshed });
|
|
346
|
+
await writeConfig(updated);
|
|
333
347
|
return refreshed;
|
|
334
348
|
} catch {
|
|
335
349
|
return cred;
|