@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 +2 -0
- package/dist/commands/whoami.d.ts +17 -0
- package/dist/commands/whoami.js +26 -3
- package/dist/lib/plan-limit-nag.d.ts +1 -1
- package/dist/lib/plan-limit-nag.js +33 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -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
|
package/dist/commands/whoami.js
CHANGED
|
@@ -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 :
|
|
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://
|
|
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://
|
|
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
|
|
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
|
|
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
|
|
94
|
-
if (
|
|
113
|
+
const status = parsePlanLimits(body);
|
|
114
|
+
if (status === null)
|
|
95
115
|
return;
|
|
96
|
-
const anyOver = Object.values(limits).some((e) => e.over);
|
|
97
|
-
lastSeen = {
|
|
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: ${
|
|
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: ${
|
|
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 {
|