@topolo/cli 0.4.4 → 0.4.5

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.
Files changed (2) hide show
  1. package/dist/index.js +75 -17
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3817,6 +3817,46 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
3817
3817
  return cred;
3818
3818
  }
3819
3819
  }
3820
+ async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
3821
+ const config = await readConfig();
3822
+ const profile = profileForEnvironment(config, environment);
3823
+ const context = profile.contexts?.[key];
3824
+ const credential = context?.credential;
3825
+ if (!credential || credential.kind !== "access_token") return credential ?? null;
3826
+ const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
3827
+ if (!credential.refreshToken || !oauthClientId) return credential;
3828
+ const now = Math.floor(Date.now() / 1e3);
3829
+ if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
3830
+ try {
3831
+ const oauth = new TopoloOAuth2({
3832
+ serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
3833
+ });
3834
+ const tokens = await oauth.refreshToken({
3835
+ clientId: oauthClientId,
3836
+ refreshToken: credential.refreshToken
3837
+ });
3838
+ const refreshed = {
3839
+ kind: "access_token",
3840
+ accessToken: tokens.access_token,
3841
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
3842
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
3843
+ };
3844
+ await writeConfig(writeProfileConfig(config, environment, {
3845
+ ...profile.activeContext === key ? { credential: refreshed } : {},
3846
+ contexts: {
3847
+ ...profile.contexts ?? {},
3848
+ [key]: {
3849
+ ...context,
3850
+ credential: refreshed,
3851
+ oauthClientId
3852
+ }
3853
+ }
3854
+ }));
3855
+ return refreshed;
3856
+ } catch {
3857
+ return credential;
3858
+ }
3859
+ }
3820
3860
  function resolveAgentName(configAgentName) {
3821
3861
  return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
3822
3862
  }
@@ -11181,6 +11221,10 @@ function hasSameAccessToken(left, right) {
11181
11221
  const rightCredential = right;
11182
11222
  return leftCredential?.kind === "access_token" && rightCredential?.kind === "access_token" && leftCredential.accessToken === rightCredential.accessToken;
11183
11223
  }
11224
+ function identityMatchesContext(me, context) {
11225
+ if (context.contextType === "personal") return me.activeType === "personal";
11226
+ return me.activeType === "organization" && me.activeOrgId === context.organizationId;
11227
+ }
11184
11228
  function registerContextCommand(program2) {
11185
11229
  const context = program2.command("context").description("Show and switch the active context (personal or an organization)");
11186
11230
  context.command("ls", { isDefault: true }).alias("list").description("List the contexts you can act in and which is active").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
@@ -11276,24 +11320,38 @@ function registerContextCommand(program2) {
11276
11320
  const key = contextKey(resolved.contextType, resolved.organizationId);
11277
11321
  const existing = profile.contexts?.[key];
11278
11322
  let newCredential;
11279
- try {
11280
- newCredential = await exchangeForContext(cred, serviceUrls, {
11281
- type: resolved.contextType,
11282
- id: resolved.organizationId
11283
- });
11284
- } catch (error) {
11285
- if (!isAuthenticationFailure(error)) throw error;
11286
- const refreshed = await refreshCredentialIfNeeded(environment, { force: true });
11287
- if (!refreshed || hasSameAccessToken(cred, refreshed)) {
11288
- throw new Error(
11289
- `The active credential could not be refreshed. Run \`topolo --env ${environment} auth login\` to sign in again.`
11290
- );
11323
+ if (existing?.credential && profile.activeContext !== key) {
11324
+ const storedCredential = await refreshContextCredentialIfNeeded(environment, key);
11325
+ if (storedCredential) {
11326
+ try {
11327
+ const storedIdentity = await fetchMe(storedCredential, serviceUrls);
11328
+ if (identityMatchesContext(storedIdentity, resolved)) {
11329
+ newCredential = storedCredential;
11330
+ }
11331
+ } catch {
11332
+ }
11333
+ }
11334
+ }
11335
+ if (!newCredential) {
11336
+ try {
11337
+ newCredential = await exchangeForContext(cred, serviceUrls, {
11338
+ type: resolved.contextType,
11339
+ id: resolved.organizationId
11340
+ });
11341
+ } catch (error) {
11342
+ if (!isAuthenticationFailure(error)) throw error;
11343
+ const refreshed = await refreshCredentialIfNeeded(environment, { force: true });
11344
+ if (!refreshed || hasSameAccessToken(cred, refreshed)) {
11345
+ throw new Error(
11346
+ `The active credential could not be refreshed. Run \`topolo --env ${environment} auth login\` to sign in again.`
11347
+ );
11348
+ }
11349
+ cred = refreshed;
11350
+ newCredential = await exchangeForContext(cred, serviceUrls, {
11351
+ type: resolved.contextType,
11352
+ id: resolved.organizationId
11353
+ });
11291
11354
  }
11292
- cred = refreshed;
11293
- newCredential = await exchangeForContext(cred, serviceUrls, {
11294
- type: resolved.contextType,
11295
- id: resolved.organizationId
11296
- });
11297
11355
  }
11298
11356
  resolved.credential = newCredential;
11299
11357
  resolved.oauthClientId = existing?.oauthClientId ?? profile.oauthClientId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Command-line interface for Topolo developers, operators, and agents, with internal first-party scaffolding available under the topolo platform namespace.",
5
5
  "type": "module",
6
6
  "bin": {