@indigoai-us/hq-cli 5.111.0 → 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 +14 -0
- package/dist/commands/whoami.d.ts +17 -0
- package/dist/commands/whoami.js +26 -3
- package/dist/lib/bot/owner-context.d.ts +8 -0
- package/dist/lib/bot/owner-context.js +28 -4
- package/dist/lib/bot/run.js +8 -1
- package/dist/lib/plan-limit-nag.d.ts +1 -1
- package/dist/lib/plan-limit-nag.js +33 -12
- package/dist/utils/cognito-session.js +49 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.111.2] — 2026-09-15
|
|
6
|
+
|
|
7
|
+
## [5.111.1] — 2026-09-14
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- A new bot's first message no longer tells its signed-in owner they are not
|
|
12
|
+
signed in. While the bot fetched its own sign-in, a check of the owner's
|
|
13
|
+
sign-in in the same process could read the bot's credentials instead and find
|
|
14
|
+
no account. Sign-in reads now wait for that to finish.
|
|
15
|
+
- When a bot cannot read its owner's account right away, it tries again before
|
|
16
|
+
answering, and never asks a signed-in owner to sign in or run a command. The
|
|
17
|
+
bot's log now records why an owner check failed.
|
|
18
|
+
|
|
5
19
|
## [5.111.0] — 2026-09-14
|
|
6
20
|
|
|
7
21
|
### Added
|
|
@@ -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) {
|
|
@@ -29,6 +29,10 @@ export type OwnerContext = {
|
|
|
29
29
|
ownerUid: string;
|
|
30
30
|
reason: string;
|
|
31
31
|
checkedAt: string;
|
|
32
|
+
/** True when the owner's sign-in exists and only reading their account failed. */
|
|
33
|
+
signedIn?: boolean;
|
|
34
|
+
/** Technical cause, for logs only; never shown to the model. */
|
|
35
|
+
detail?: string;
|
|
32
36
|
} | {
|
|
33
37
|
status: "mismatch";
|
|
34
38
|
ownerUid: string;
|
|
@@ -48,8 +52,12 @@ export interface OwnerLookupIo {
|
|
|
48
52
|
status: string;
|
|
49
53
|
}>>;
|
|
50
54
|
now?: () => Date;
|
|
55
|
+
/** Test seam for the retry pause. */
|
|
56
|
+
sleep?: (ms: number) => Promise<void>;
|
|
51
57
|
}
|
|
52
58
|
export declare const defaultOwnerLookupIo: OwnerLookupIo;
|
|
59
|
+
/** A read right after sign-in or bot start can fail once; try again before reporting it. */
|
|
60
|
+
export declare const OWNER_LOOKUP_RETRY_DELAYS_MS: number[];
|
|
53
61
|
export declare function lookupOwnerContext(ownerUid: string, io?: OwnerLookupIo): Promise<OwnerContext>;
|
|
54
62
|
/** Successful lookups are reused for 5 minutes, failures retried after 1 minute. */
|
|
55
63
|
export declare const OWNER_CONTEXT_TTL_MS: number;
|
|
@@ -33,6 +33,23 @@ export const defaultOwnerLookupIo = {
|
|
|
33
33
|
return data.memberships;
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
|
+
/** A read right after sign-in or bot start can fail once; try again before reporting it. */
|
|
37
|
+
export const OWNER_LOOKUP_RETRY_DELAYS_MS = [1_000, 2_000];
|
|
38
|
+
async function withRetry(fn, io) {
|
|
39
|
+
const sleep = io.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
40
|
+
let lastErr;
|
|
41
|
+
for (const delay of [0, ...OWNER_LOOKUP_RETRY_DELAYS_MS]) {
|
|
42
|
+
if (delay)
|
|
43
|
+
await sleep(delay);
|
|
44
|
+
try {
|
|
45
|
+
return await fn();
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
lastErr = err;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw lastErr;
|
|
52
|
+
}
|
|
36
53
|
export async function lookupOwnerContext(ownerUid, io = defaultOwnerLookupIo) {
|
|
37
54
|
const checkedAt = (io.now ?? (() => new Date()))().toISOString();
|
|
38
55
|
let token;
|
|
@@ -44,22 +61,23 @@ export async function lookupOwnerContext(ownerUid, io = defaultOwnerLookupIo) {
|
|
|
44
61
|
}
|
|
45
62
|
let signedInAs;
|
|
46
63
|
try {
|
|
47
|
-
signedInAs = await io.callerPersonUid(token);
|
|
64
|
+
signedInAs = await withRetry(() => io.callerPersonUid(token), io);
|
|
48
65
|
}
|
|
49
66
|
catch (err) {
|
|
50
|
-
|
|
67
|
+
// The raw error ("…Sign in to HQ once…") would read to the model as "not signed in".
|
|
68
|
+
return { status: "unavailable", ownerUid, reason: "their HQ account could not be read just now", detail: message(err), checkedAt, signedIn: true };
|
|
51
69
|
}
|
|
52
70
|
if (signedInAs !== ownerUid)
|
|
53
71
|
return { status: "mismatch", ownerUid, signedInAs, checkedAt };
|
|
54
72
|
try {
|
|
55
|
-
const rows = await io.memberships(token);
|
|
73
|
+
const rows = await withRetry(() => io.memberships(token), io);
|
|
56
74
|
const companies = rows
|
|
57
75
|
.filter((m) => m.status === "active")
|
|
58
76
|
.map((m) => ({ companyUid: m.companyUid, ...(m.companySlug ? { companySlug: m.companySlug } : {}), role: m.role }));
|
|
59
77
|
return { status: "ok", ownerUid, companies, checkedAt };
|
|
60
78
|
}
|
|
61
79
|
catch (err) {
|
|
62
|
-
return { status: "unavailable", ownerUid, reason: `could not read your owner's companies (${message(err)})`, checkedAt };
|
|
80
|
+
return { status: "unavailable", ownerUid, reason: `could not read your owner's companies (${message(err)})`, checkedAt, signedIn: true };
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
/** Successful lookups are reused for 5 minutes, failures retried after 1 minute. */
|
|
@@ -141,6 +159,12 @@ export function ownerContextBlock(ctx, agentUid) {
|
|
|
141
159
|
`The HQ sign-in on this computer belongs to ${ctx.signedInAs}, not your owner, so your owner's companies could not be checked. ` +
|
|
142
160
|
"Do not say which companies your owner belongs to; tell them you could not check and why.";
|
|
143
161
|
}
|
|
162
|
+
else if (ctx.signedIn) {
|
|
163
|
+
body =
|
|
164
|
+
`Your owner IS signed in to HQ on this computer, but their companies could not be checked: ${ctx.reason}. ` +
|
|
165
|
+
"Do not guess and do not say they have no company. Do not ask them to sign in or to run any command: " +
|
|
166
|
+
"say you could not read their account just now and that you will check again on their next message.";
|
|
167
|
+
}
|
|
144
168
|
else {
|
|
145
169
|
body =
|
|
146
170
|
`Your owner's companies could not be checked: ${ctx.reason}. ` +
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -311,7 +311,14 @@ export async function runBot(deps) {
|
|
|
311
311
|
// by other people, who must not be handed the owner's companies.
|
|
312
312
|
if (deps.ownerContext && sessionScope === "dm") {
|
|
313
313
|
try {
|
|
314
|
-
|
|
314
|
+
const owner = await deps.ownerContext();
|
|
315
|
+
if (owner.status === "unavailable") {
|
|
316
|
+
log("warn", `owner context unavailable: ${owner.reason}${owner.detail ? ` (${owner.detail})` : ""}`);
|
|
317
|
+
}
|
|
318
|
+
else if (owner.status === "mismatch") {
|
|
319
|
+
log("warn", `owner context mismatch: signed in as ${owner.signedInAs}`);
|
|
320
|
+
}
|
|
321
|
+
prompt = `${ownerContextBlock(owner, config.agentUid)}\n\n${prompt}`;
|
|
315
322
|
}
|
|
316
323
|
catch (err) {
|
|
317
324
|
log("warn", `owner context unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -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 {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* HQ_COGNITO_CALLBACK_PORT — Loopback OAuth callback port
|
|
19
19
|
* HQ_VAULT_API_URL — vault-service API Gateway URL
|
|
20
20
|
*/
|
|
21
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
21
22
|
import * as fs from "fs";
|
|
22
23
|
import * as os from "os";
|
|
23
24
|
import * as path from "path";
|
|
@@ -496,19 +497,39 @@ export function describeCognitoTokenSource(opts = {}) {
|
|
|
496
497
|
* `HQ_MACHINE_TOKEN_STATE_DIR` instead. Restores the prior env afterwards.
|
|
497
498
|
*/
|
|
498
499
|
export async function withMachineTokenStateDir(fn, opts = {}) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
500
|
+
return withTokenStateDirSection(async () => {
|
|
501
|
+
const home = opts.home ?? os.homedir();
|
|
502
|
+
const env = opts.env ?? process.env;
|
|
503
|
+
const prev = process.env.HQ_STATE_DIR;
|
|
504
|
+
process.env.HQ_STATE_DIR = machineTokenStateDir(home, env);
|
|
505
|
+
try {
|
|
506
|
+
return await fn();
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
if (prev === undefined)
|
|
510
|
+
delete process.env.HQ_STATE_DIR;
|
|
511
|
+
else
|
|
512
|
+
process.env.HQ_STATE_DIR = prev;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* The token cache location (`HQ_STATE_DIR`) is process-wide, and a machine mint
|
|
518
|
+
* points it at the machine cache across an await. A person-session read in the
|
|
519
|
+
* same process while that mint is in flight would load the MACHINE tokens: a
|
|
520
|
+
* local bot's resident (machine identity for its own calls) checking its
|
|
521
|
+
* owner's sign-in at startup got the bot's token and "No person entity found".
|
|
522
|
+
* Every section that swaps or reads the cache location runs one at a time;
|
|
523
|
+
* nested sections (a person read falling back to a machine mint) run inline.
|
|
524
|
+
*/
|
|
525
|
+
const tokenStateDirSection = new AsyncLocalStorage();
|
|
526
|
+
let tokenStateDirTail = Promise.resolve();
|
|
527
|
+
function withTokenStateDirSection(fn) {
|
|
528
|
+
if (tokenStateDirSection.getStore())
|
|
529
|
+
return fn();
|
|
530
|
+
const run = tokenStateDirTail.then(() => tokenStateDirSection.run(true, fn));
|
|
531
|
+
tokenStateDirTail = run.catch(() => undefined);
|
|
532
|
+
return run;
|
|
512
533
|
}
|
|
513
534
|
async function ensureMachineTokens() {
|
|
514
535
|
return withMachineTokenStateDir(() => getValidMachineTokens(DEFAULT_COGNITO));
|
|
@@ -567,6 +588,11 @@ function markPersonSessionRejected(accessToken) {
|
|
|
567
588
|
* a browser.
|
|
568
589
|
*/
|
|
569
590
|
export async function ensureCognitoToken(options = {}) {
|
|
591
|
+
if (wantsMachineCognitoTokens(options))
|
|
592
|
+
return ensureCognitoTokenUnserialized(options);
|
|
593
|
+
return withTokenStateDirSection(() => ensureCognitoTokenUnserialized(options));
|
|
594
|
+
}
|
|
595
|
+
async function ensureCognitoTokenUnserialized(options) {
|
|
570
596
|
const interactive = options.interactive ?? true;
|
|
571
597
|
// Machine identities (company agents) mint sessions on demand via
|
|
572
598
|
// USER_PASSWORD_AUTH — no refresh-token dance, no browser. The vault API's
|
|
@@ -631,6 +657,11 @@ export async function ensureCognitoToken(options = {}) {
|
|
|
631
657
|
* identical to `ensureCognitoToken` there.
|
|
632
658
|
*/
|
|
633
659
|
export async function ensureCognitoIdToken(options = {}) {
|
|
660
|
+
if (wantsMachineCognitoTokens(options))
|
|
661
|
+
return ensureCognitoIdTokenUnserialized(options);
|
|
662
|
+
return withTokenStateDirSection(() => ensureCognitoIdTokenUnserialized(options));
|
|
663
|
+
}
|
|
664
|
+
async function ensureCognitoIdTokenUnserialized(options) {
|
|
634
665
|
const interactive = options.interactive ?? true;
|
|
635
666
|
if (wantsMachineCognitoTokens(options)) {
|
|
636
667
|
const machine = await ensureMachineTokensMaybeFallback(options);
|
|
@@ -704,6 +735,11 @@ export function buildVaultConfig(authToken) {
|
|
|
704
735
|
* reason string and can decide what to do next. Never opens a browser.
|
|
705
736
|
*/
|
|
706
737
|
export async function refreshCachedSession(options = {}) {
|
|
738
|
+
if (wantsMachineCognitoTokens(options))
|
|
739
|
+
return refreshCachedSessionUnserialized(options);
|
|
740
|
+
return withTokenStateDirSection(() => refreshCachedSessionUnserialized(options));
|
|
741
|
+
}
|
|
742
|
+
async function refreshCachedSessionUnserialized(options) {
|
|
707
743
|
// Machine identities have no refresh token; ensure a valid cached machine
|
|
708
744
|
// session without forcing a re-mint when the cache is already healthy.
|
|
709
745
|
// Tokens land under the dedicated machine/daemon state dir.
|