@indigoai-us/hq-cli 5.111.1 → 5.111.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.111.2] — 2026-09-15
6
+
5
7
  ## [5.111.1] — 2026-09-14
6
8
 
7
9
  ### Fixed
@@ -2,5 +2,22 @@
2
2
  * hq whoami — displays current user or 'not logged in'
3
3
  */
4
4
  import { Command } from 'commander';
5
+ export interface WhoamiTokenIdentity {
6
+ email?: string;
7
+ sub?: string;
8
+ delegatedEmail?: string;
9
+ delegatedSub?: string;
10
+ entityType?: string;
11
+ entityUid?: string;
12
+ }
13
+ export interface WhoamiDisplayIdentity {
14
+ email?: string;
15
+ sub?: string;
16
+ }
17
+ /**
18
+ * Resolves the person represented by an ID token without pairing a delegated
19
+ * email with the token subject of the delegating machine.
20
+ */
21
+ export declare function resolveWhoamiIdentity(identity: WhoamiTokenIdentity): WhoamiDisplayIdentity;
5
22
  export declare function registerWhoamiCommand(program: Command): void;
6
23
  //# sourceMappingURL=whoami.d.ts.map
@@ -5,11 +5,33 @@ import chalk from 'chalk';
5
5
  import { loadCachedTokens, isExpiring, loadMachineCreds, } from '@indigoai-us/hq-cloud';
6
6
  import { peekIdToken as decodeIdToken } from "../utils/id-token.js";
7
7
  import { loadMachineCachedTokens, resolveCognitoTokenSource } from "../utils/cognito-session.js";
8
+ /**
9
+ * Resolves the person represented by an ID token without pairing a delegated
10
+ * email with the token subject of the delegating machine.
11
+ */
12
+ export function resolveWhoamiIdentity(identity) {
13
+ if (identity.email !== undefined && identity.sub !== undefined) {
14
+ return { email: identity.email, sub: identity.sub };
15
+ }
16
+ if (identity.delegatedEmail !== undefined && identity.delegatedSub !== undefined) {
17
+ return { email: identity.delegatedEmail, sub: identity.delegatedSub };
18
+ }
19
+ if (identity.email !== undefined || identity.sub !== undefined) {
20
+ return { email: identity.email, sub: identity.sub };
21
+ }
22
+ return {};
23
+ }
8
24
  function peekIdToken(idToken) {
9
25
  const decoded = decodeIdToken(idToken);
10
26
  return {
11
27
  email: typeof decoded.email === "string" ? decoded.email : undefined,
12
28
  sub: typeof decoded.sub === "string" ? decoded.sub : undefined,
29
+ delegatedEmail: typeof decoded["custom:delegatedEmail"] === "string"
30
+ ? decoded["custom:delegatedEmail"]
31
+ : undefined,
32
+ delegatedSub: typeof decoded["custom:delegatedSub"] === "string"
33
+ ? decoded["custom:delegatedSub"]
34
+ : undefined,
13
35
  entityType: typeof decoded["custom:entityType"] === "string"
14
36
  ? decoded["custom:entityType"]
15
37
  : undefined,
@@ -34,6 +56,7 @@ export function registerWhoamiCommand(program) {
34
56
  const machineCreds = machine ? loadMachineCreds() : null;
35
57
  const identityCache = machine ? loadMachineCachedTokens() : cached;
36
58
  const identity = identityCache ? peekIdToken(identityCache.idToken) : {};
59
+ const displayedIdentity = resolveWhoamiIdentity(identity);
37
60
  const claims = identityCache ? decodeIdToken(identityCache.idToken) : {};
38
61
  // Modern credentials carry the canonical UID. Legacy caches may only
39
62
  // supply a UID when their subject is bound to the selected username.
@@ -48,7 +71,7 @@ export function registerWhoamiCommand(program) {
48
71
  schemaVersion: 1,
49
72
  authenticated: machine ? Boolean(machineCreds) : Boolean(cached && !isExpiring(cached, 0)),
50
73
  tokenSource: machine ? 'machine' : 'person',
51
- email: machine ? null : identity.email ?? null,
74
+ email: machine ? null : displayedIdentity.email ?? null,
52
75
  personUid: !machine && identity.entityType === 'person' ? identity.entityUid ?? null : null,
53
76
  agentUid,
54
77
  username: machineCreds?.username ?? null,
@@ -72,11 +95,11 @@ export function registerWhoamiCommand(program) {
72
95
  return;
73
96
  }
74
97
  if (isExpiring(cached, 0)) {
75
- const who = peekIdToken(cached.idToken).email ?? 'unknown';
98
+ const who = resolveWhoamiIdentity(peekIdToken(cached.idToken)).email ?? 'unknown';
76
99
  console.log(chalk.yellow(`Session expired for ${who}. Run 'hq login' to re-authenticate.`));
77
100
  return;
78
101
  }
79
- const { email, sub } = peekIdToken(cached.idToken);
102
+ const { email, sub } = resolveWhoamiIdentity(peekIdToken(cached.idToken));
80
103
  console.log(`Logged in as ${email ?? 'unknown'}${sub ? ` (${sub})` : ''}`);
81
104
  }
82
105
  catch (error) {
@@ -15,7 +15,7 @@
15
15
  * Additive only — never throws, never touches `process.exitCode`, never
16
16
  * writes to stdout. Env off-switch: `HQ_NO_PLAN_LIMIT_NAG=1`.
17
17
  */
18
- export declare const PLAN_LIMIT_UPGRADE_URL = "https://app.indigo-hq.com/billing/upgrade";
18
+ export declare const PLAN_LIMIT_UPGRADE_URL = "https://hq.computer/billing/upgrade";
19
19
  export interface PlanLimitEntry {
20
20
  used: number;
21
21
  limit: number;
@@ -19,7 +19,7 @@ import chalk from "chalk";
19
19
  import * as fs from "node:fs";
20
20
  import * as os from "node:os";
21
21
  import * as path from "node:path";
22
- export const PLAN_LIMIT_UPGRADE_URL = "https://app.indigo-hq.com/billing/upgrade";
22
+ export const PLAN_LIMIT_UPGRADE_URL = "https://hq.computer/billing/upgrade";
23
23
  const DAY_MS = 24 * 60 * 60 * 1000;
24
24
  /** Module-level last-seen cell — overwritten by each successful parse. */
25
25
  let lastSeen = null;
@@ -58,9 +58,24 @@ function parseEntry(value) {
58
58
  }
59
59
  return { used: rec.used, limit: rec.limit, over: rec.over };
60
60
  }
61
+ /** Validate the server-provided optional upgrade URL without throwing. */
62
+ function parseUpgradeUrl(value) {
63
+ if (typeof value !== "string")
64
+ return null;
65
+ try {
66
+ const parsed = new URL(value);
67
+ // hq-pro permits an environment-configured console base URL, so stages
68
+ // cannot use a production-host allowlist. HTTPS is the trust boundary.
69
+ return parsed.protocol === "https:" ? parsed.toString() : null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
61
75
  /**
62
76
  * Defensively parse a decoded JSON body for a well-formed top-level
63
- * `planLimits` object. Malformed or absent → null. Never throws.
77
+ * `planLimits` object and its optional `upgradeUrl`. Malformed or absent →
78
+ * null. Never throws.
64
79
  */
65
80
  function parsePlanLimits(body) {
66
81
  if (body === null || typeof body !== "object" || Array.isArray(body)) {
@@ -81,7 +96,12 @@ function parsePlanLimits(body) {
81
96
  out[key] = entry;
82
97
  anyValid = true;
83
98
  }
84
- return anyValid ? out : null;
99
+ return anyValid
100
+ ? {
101
+ limits: out,
102
+ upgradeUrl: parseUpgradeUrl(planLimits.upgradeUrl),
103
+ }
104
+ : null;
85
105
  }
86
106
  /**
87
107
  * Record plan-limit status from a decoded JSON response body.
@@ -90,11 +110,11 @@ function parsePlanLimits(body) {
90
110
  */
91
111
  export function recordPlanLimitStatus(body) {
92
112
  try {
93
- const limits = parsePlanLimits(body);
94
- if (limits === null)
113
+ const status = parsePlanLimits(body);
114
+ if (status === null)
95
115
  return;
96
- const anyOver = Object.values(limits).some((e) => e.over);
97
- lastSeen = { limits, anyOver };
116
+ const anyOver = Object.values(status.limits).some((e) => e.over);
117
+ lastSeen = { ...status, anyOver };
98
118
  }
99
119
  catch {
100
120
  // Never throw from record path.
@@ -150,9 +170,9 @@ function writeShownAt(statePath, shownAt) {
150
170
  function withinDayWindow(shownAt, nowMs) {
151
171
  return nowMs - shownAt < DAY_MS;
152
172
  }
153
- function buildOverBox(overEntries) {
173
+ function buildOverBox(overEntries, upgradeUrl) {
154
174
  const title = "⚠ HQ plan limit exceeded";
155
- const upgrade = `Upgrade: ${PLAN_LIMIT_UPGRADE_URL}`;
175
+ const upgrade = `Upgrade: ${upgradeUrl}`;
156
176
  const resourceLines = overEntries.map(([key, entry]) => ` ${key}: ${entry.used}/${entry.limit}`);
157
177
  const contentLines = [title, "", ...resourceLines, "", upgrade];
158
178
  const innerWidth = Math.max(...contentLines.map((l) => l.length), 40);
@@ -178,7 +198,8 @@ export function emitPlanLimitNag(opts = {}) {
178
198
  const write = opts.write ?? ((s) => process.stderr.write(s));
179
199
  const now = opts.now ?? (() => new Date());
180
200
  const statePath = opts.statePath ?? defaultStatePath();
181
- const { limits, anyOver } = lastSeen;
201
+ const { limits, anyOver, upgradeUrl } = lastSeen;
202
+ const resolvedUpgradeUrl = upgradeUrl ?? PLAN_LIMIT_UPGRADE_URL;
182
203
  const entries = Object.entries(limits);
183
204
  if (entries.length === 0)
184
205
  return;
@@ -191,7 +212,7 @@ export function emitPlanLimitNag(opts = {}) {
191
212
  return;
192
213
  overShownThisSession = true;
193
214
  const overEntries = entries.filter(([, e]) => e.over);
194
- const box = buildOverBox(overEntries);
215
+ const box = buildOverBox(overEntries, resolvedUpgradeUrl);
195
216
  write(chalk.yellow(box) + "\n");
196
217
  writeShownAt(statePath, nowMs);
197
218
  return;
@@ -203,7 +224,7 @@ export function emitPlanLimitNag(opts = {}) {
203
224
  if (worst === null)
204
225
  return;
205
226
  warningShownThisSession = true;
206
- const line = `⚠ HQ free plan: ${formatEntryLine(worst.key, worst.entry)}. Upgrade: ${PLAN_LIMIT_UPGRADE_URL}`;
227
+ const line = `⚠ HQ free plan: ${formatEntryLine(worst.key, worst.entry)}. Upgrade: ${resolvedUpgradeUrl}`;
207
228
  write(chalk.yellow(line) + "\n");
208
229
  }
209
230
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.111.1",
3
+ "version": "5.111.2",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {