@indigoai-us/hq-cli 5.108.6 → 5.108.7
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 +17 -0
- package/dist/commands/auth.js +11 -4
- package/dist/commands/whoami.js +7 -2
- package/dist/utils/cognito-session.d.ts +58 -16
- package/dist/utils/cognito-session.js +229 -39
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.108.7] — 2026-09-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fresh agent boxes and Outposts no longer fail `hq auth refresh` with "No
|
|
10
|
+
valid session: no cached session" when `HQ_MACHINE_CREDS_FILE` is unset.
|
|
11
|
+
Regression since 5.108.2: machine-token minting required that env (or an
|
|
12
|
+
explicit `{ tokenSource: "machine" }`), but hq-pro cloud-init never exports
|
|
13
|
+
it, so bootstrap aborted after sixty refresh failures. When the env is
|
|
14
|
+
unset, there is no usable person session at `~/.hq/cognito-tokens.json`, and
|
|
15
|
+
the default `~/.hq-agent/machine-creds.json` is readable, the CLI now mints
|
|
16
|
+
from that file (same daemon-state-dir cache as the env opt-in) and logs
|
|
17
|
+
`machine identity from default creds file`. A present person session still
|
|
18
|
+
wins so a human login is never flipped onto machine tokens; explicit
|
|
19
|
+
`tokenSource: "person"` never uses machine creds. `hq auth status` and
|
|
20
|
+
`hq whoami` report the resolved token source.
|
|
21
|
+
|
|
5
22
|
## [5.108.6] — 2026-09-05
|
|
6
23
|
|
|
7
24
|
### Added
|
package/dist/commands/auth.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* by the deploy + sync skills.
|
|
15
15
|
*/
|
|
16
16
|
import chalk from "chalk";
|
|
17
|
-
import { browserLogin, clearCachedTokens, loadCachedTokens, isExpiring,
|
|
18
|
-
import { DEFAULT_COGNITO, refreshCachedSession, } from "../utils/cognito-session.js";
|
|
17
|
+
import { browserLogin, clearCachedTokens, loadCachedTokens, isExpiring, loadMachineCreds, CognitoAuthError, } from "@indigoai-us/hq-cloud";
|
|
18
|
+
import { DEFAULT_COGNITO, refreshCachedSession, resolveCognitoTokenSource, } from "../utils/cognito-session.js";
|
|
19
19
|
import { cognitoConfigForLoginProvider } from "../utils/login-provider.js";
|
|
20
20
|
import { callbackPortBusyGuidance, DEFAULT_CALLBACK_PORT, isCallbackPortBusy, } from "../utils/callback-port-busy.js";
|
|
21
21
|
/**
|
|
@@ -56,7 +56,7 @@ export function registerAuthCommands(program) {
|
|
|
56
56
|
.description("Sign in to HQ — opens the Cognito Hosted UI and caches tokens locally")
|
|
57
57
|
.option("--provider <provider>", "OAuth provider to use: google, microsoft, or picker")
|
|
58
58
|
.action(async (options) => {
|
|
59
|
-
if (
|
|
59
|
+
if (resolveCognitoTokenSource() === "machine") {
|
|
60
60
|
console.log(chalk.green(`Running as ${machineIdentityLabel()} — sessions mint automatically; no browser login needed.`));
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
@@ -119,16 +119,22 @@ export function registerAuthCommands(program) {
|
|
|
119
119
|
.command("status")
|
|
120
120
|
.description("Show whether a valid HQ session is cached")
|
|
121
121
|
.action(() => {
|
|
122
|
-
|
|
122
|
+
// Effective source: env opt-in, fresh-box default-creds when no usable
|
|
123
|
+
// person session, or after a definitive person-refresh rejection marked
|
|
124
|
+
// the person cache rejected (transient refresh errors still report person).
|
|
125
|
+
const tokenSource = resolveCognitoTokenSource();
|
|
126
|
+
const machine = tokenSource === "machine";
|
|
123
127
|
const cached = loadCachedTokens();
|
|
124
128
|
if (!cached) {
|
|
125
129
|
if (machine) {
|
|
126
130
|
// No cached session yet, but machine creds mint one on demand —
|
|
127
131
|
// report ready, not signed-out.
|
|
128
132
|
console.log(chalk.green(`${machineIdentityLabel()} — no cached session yet (mints automatically on first use)`));
|
|
133
|
+
console.log(chalk.dim(`token source: ${tokenSource}`));
|
|
129
134
|
return;
|
|
130
135
|
}
|
|
131
136
|
console.log(chalk.yellow("No cached HQ session — run `hq auth login`"));
|
|
137
|
+
console.log(chalk.dim(`token source: ${tokenSource}`));
|
|
132
138
|
process.exit(1);
|
|
133
139
|
}
|
|
134
140
|
const claims = peekIdToken(cached.idToken);
|
|
@@ -140,6 +146,7 @@ export function registerAuthCommands(program) {
|
|
|
140
146
|
console.log(expiring
|
|
141
147
|
? chalk.yellow(`${label}HQ session cached but expiring (expiresAt=${cached.expiresAt})`)
|
|
142
148
|
: chalk.green(`${label}HQ session valid (expiresAt=${cached.expiresAt})`));
|
|
149
|
+
console.log(chalk.dim(`token source: ${tokenSource}`));
|
|
143
150
|
});
|
|
144
151
|
}
|
|
145
152
|
//# sourceMappingURL=auth.js.map
|
package/dist/commands/whoami.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* hq whoami — displays current user or 'not logged in'
|
|
3
3
|
*/
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import { loadCachedTokens, isExpiring,
|
|
5
|
+
import { loadCachedTokens, isExpiring, loadMachineCreds, } from '@indigoai-us/hq-cloud';
|
|
6
6
|
import { peekIdToken as decodeIdToken } from "../utils/id-token.js";
|
|
7
|
+
import { resolveCognitoTokenSource } from "../utils/cognito-session.js";
|
|
7
8
|
function peekIdToken(idToken) {
|
|
8
9
|
const decoded = decodeIdToken(idToken);
|
|
9
10
|
return {
|
|
@@ -23,7 +24,10 @@ export function registerWhoamiCommand(program) {
|
|
|
23
24
|
.description('Show the currently authenticated user')
|
|
24
25
|
.action(async () => {
|
|
25
26
|
try {
|
|
26
|
-
|
|
27
|
+
// Effective source (same as auth status / ensureCognitoToken): env
|
|
28
|
+
// opt-in, fresh-box default-creds when no usable person session, or
|
|
29
|
+
// after a definitive person-refresh rejection marked the session rejected.
|
|
30
|
+
const machine = resolveCognitoTokenSource() === "machine";
|
|
27
31
|
const cached = loadCachedTokens();
|
|
28
32
|
if (machine) {
|
|
29
33
|
// Machine identities (company agents) mint sessions on demand from
|
|
@@ -34,6 +38,7 @@ export function registerWhoamiCommand(program) {
|
|
|
34
38
|
? peekIdToken(cached.idToken).entityUid
|
|
35
39
|
: undefined;
|
|
36
40
|
console.log(`Machine identity ${username}${entityUid ? ` (agent ${entityUid})` : ''} — sessions mint automatically`);
|
|
41
|
+
console.log(`token source: machine`);
|
|
37
42
|
return;
|
|
38
43
|
}
|
|
39
44
|
if (!cached) {
|
|
@@ -93,31 +93,64 @@ export declare function machineTokenStateDir(home?: string, env?: NodeJS.Process
|
|
|
93
93
|
export declare function machineTokenCacheFile(home?: string, env?: NodeJS.ProcessEnv): string;
|
|
94
94
|
/**
|
|
95
95
|
* True when `HQ_MACHINE_CREDS_FILE` is explicitly set (non-empty) in `env`.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
96
|
+
* Explicit env remains the primary opt-in for daemon units. Fresh agent boxes
|
|
97
|
+
* without the env still mint via {@link wantsMachineCognitoTokens}'s
|
|
98
|
+
* default-file fallback when no usable person session exists.
|
|
99
99
|
*/
|
|
100
100
|
export declare function isMachineCredsFileEnvSet(env?: NodeJS.ProcessEnv): boolean;
|
|
101
|
+
/** Default machine-creds path hq-cloud also checks when the env is unset. */
|
|
102
|
+
export declare function defaultMachineCredsFile(home?: string): string;
|
|
101
103
|
export type EnsureCognitoTokenOptions = {
|
|
102
104
|
interactive?: boolean;
|
|
103
105
|
/**
|
|
104
106
|
* Force machine-credential minting (`"machine"`) or the person login cache
|
|
105
|
-
* (`"person"`). When omitted, machine minting is used
|
|
106
|
-
* `HQ_MACHINE_CREDS_FILE` is
|
|
107
|
+
* (`"person"`). When omitted, machine minting is used if
|
|
108
|
+
* `HQ_MACHINE_CREDS_FILE` is set, or (fresh-box fallback) the default
|
|
109
|
+
* `~/.hq-agent/machine-creds.json` is readable and no person session is
|
|
110
|
+
* cached. Explicit `"person"` never uses machine creds.
|
|
107
111
|
*/
|
|
108
112
|
tokenSource?: CognitoTokenSource;
|
|
109
113
|
};
|
|
114
|
+
/**
|
|
115
|
+
* On-disk person Cognito cache path for `env` (`HQ_STATE_DIR` or `~/.hq`).
|
|
116
|
+
* Deliberately does not use hq-cloud's `loadCachedTokens()`: after a machine
|
|
117
|
+
* mint in this process, that helper returns in-memory machine tokens when the
|
|
118
|
+
* person file is absent, which would falsely block the fresh-box fallback.
|
|
119
|
+
*/
|
|
120
|
+
export declare function personTokenCacheFile(home?: string, env?: NodeJS.ProcessEnv): string;
|
|
121
|
+
/**
|
|
122
|
+
* True when a usable person Cognito cache file exists under the effective
|
|
123
|
+
* `HQ_STATE_DIR` (or `~/.hq`). Presence alone keeps human commands on the
|
|
124
|
+
* person path even when the default machine-creds file is also on disk
|
|
125
|
+
* (no-flip guarantee).
|
|
126
|
+
*/
|
|
127
|
+
export declare function hasPersonCachedSession(env?: NodeJS.ProcessEnv): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Fresh-box / Outpost bootstrap: env unset, not forced to person, no person
|
|
130
|
+
* cache, and the default machine-creds file is readable. Distinct from
|
|
131
|
+
* explicit `HQ_MACHINE_CREDS_FILE` / `{ tokenSource: "machine" }` opt-in.
|
|
132
|
+
*/
|
|
133
|
+
export declare function isDefaultMachineCredsFallback(options?: Pick<EnsureCognitoTokenOptions, "tokenSource">, env?: NodeJS.ProcessEnv): boolean;
|
|
110
134
|
/**
|
|
111
135
|
* Whether this call should mint/cache via machine creds (USER_PASSWORD_AUTH).
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
136
|
+
*
|
|
137
|
+
* Machine path when:
|
|
138
|
+
* - explicit `{ tokenSource: "machine" }` and creds are readable, or
|
|
139
|
+
* - `HQ_MACHINE_CREDS_FILE` is set and readable, or
|
|
140
|
+
* - fresh-box fallback: env unset, no person cache, default
|
|
141
|
+
* `~/.hq-agent/machine-creds.json` readable.
|
|
142
|
+
*
|
|
143
|
+
* Explicit `{ tokenSource: "person" }` never uses machine creds. A present
|
|
144
|
+
* person cache keeps the person path even when the default creds file exists
|
|
145
|
+
* (do not flip a human's commands onto machine tokens).
|
|
115
146
|
*/
|
|
116
147
|
export declare function wantsMachineCognitoTokens(options?: Pick<EnsureCognitoTokenOptions, "tokenSource">, env?: NodeJS.ProcessEnv): boolean;
|
|
117
148
|
/**
|
|
118
|
-
* Which Cognito token source this process will use for doctor /
|
|
119
|
-
*
|
|
120
|
-
*
|
|
149
|
+
* Which Cognito token source this process will use for doctor / status / whoami.
|
|
150
|
+
* Matches {@link wantsMachineCognitoTokens}: env opt-in, or default-file
|
|
151
|
+
* fallback when no usable person session is cached (absent, or marked rejected
|
|
152
|
+
* after a definitive Cognito refresh refusal). Transient refresh failures do
|
|
153
|
+
* not flip this to machine.
|
|
121
154
|
*/
|
|
122
155
|
export declare function resolveCognitoTokenSource(env?: NodeJS.ProcessEnv): CognitoTokenSource;
|
|
123
156
|
/** Derive actor kind from an ID token's custom:entity* claims (no verify). */
|
|
@@ -143,19 +176,28 @@ export declare function describeCognitoTokenSource(opts?: {
|
|
|
143
176
|
* Run `fn` with `HQ_STATE_DIR` pointed at the machine/daemon token cache so
|
|
144
177
|
* hq-cloud's mint writes never touch the person login file.
|
|
145
178
|
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
179
|
+
* Always redirects — even when the caller already set `HQ_STATE_DIR` (person
|
|
180
|
+
* cache / tests). Override the machine cache location with
|
|
181
|
+
* `HQ_MACHINE_TOKEN_STATE_DIR` instead. Restores the prior env afterwards.
|
|
148
182
|
*/
|
|
149
183
|
export declare function withMachineTokenStateDir<T>(fn: () => Promise<T>, opts?: {
|
|
150
184
|
home?: string;
|
|
151
185
|
env?: NodeJS.ProcessEnv;
|
|
152
186
|
}): Promise<T>;
|
|
187
|
+
/**
|
|
188
|
+
* True when a person-token refresh failed with a definitive Cognito auth
|
|
189
|
+
* rejection (refresh token invalid/revoked). Transient network/5xx/timeouts
|
|
190
|
+
* must NOT trigger machine-identity fallback.
|
|
191
|
+
*/
|
|
192
|
+
export declare function isDefinitivePersonAuthRejection(err: unknown): boolean;
|
|
153
193
|
/**
|
|
154
194
|
* Return a non-expired Cognito access token, refreshing or browser-logging-in
|
|
155
195
|
* as needed. Person cache lives at ~/.hq/cognito-tokens.json. Machine minting
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
196
|
+
* uses `HQ_MACHINE_CREDS_FILE` / `{ tokenSource: "machine" }`, or the fresh-box
|
|
197
|
+
* fallback when the default creds file is readable and no person session
|
|
198
|
+
* exists (or person refresh fails with a definitive auth rejection).
|
|
199
|
+
* Machine tokens always cache under the dedicated machine/daemon state dir
|
|
200
|
+
* (never the caller's `HQ_STATE_DIR` person cache).
|
|
159
201
|
*
|
|
160
202
|
* Pass `interactive: false` from automated contexts (e.g. the `hq-auth-refresh`
|
|
161
203
|
* bin invoked by the deploy skill) where failing fast is better than opening
|
|
@@ -23,7 +23,7 @@ import * as os from "os";
|
|
|
23
23
|
import * as path from "path";
|
|
24
24
|
import * as yaml from "js-yaml";
|
|
25
25
|
import chalk from "chalk";
|
|
26
|
-
import { loadCachedTokens, isExpiring, refreshTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, } from "@indigoai-us/hq-cloud";
|
|
26
|
+
import { loadCachedTokens, isExpiring, refreshTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, CognitoRefreshError, accessTokenFingerprint, invalidateCachedTokensByFingerprint, } from "@indigoai-us/hq-cloud";
|
|
27
27
|
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
28
28
|
import { daemonDir } from "../lib/mesh/live/daemon/paths.js";
|
|
29
29
|
import { workMeshRoot } from "../lib/mesh/live/paths.js";
|
|
@@ -248,40 +248,36 @@ export function machineTokenCacheFile(home = os.homedir(), env = process.env) {
|
|
|
248
248
|
}
|
|
249
249
|
/**
|
|
250
250
|
* True when `HQ_MACHINE_CREDS_FILE` is explicitly set (non-empty) in `env`.
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
251
|
+
* Explicit env remains the primary opt-in for daemon units. Fresh agent boxes
|
|
252
|
+
* without the env still mint via {@link wantsMachineCognitoTokens}'s
|
|
253
|
+
* default-file fallback when no usable person session exists.
|
|
254
254
|
*/
|
|
255
255
|
export function isMachineCredsFileEnvSet(env = process.env) {
|
|
256
256
|
const raw = env.HQ_MACHINE_CREDS_FILE;
|
|
257
257
|
return typeof raw === "string" && raw.trim().length > 0;
|
|
258
258
|
}
|
|
259
|
+
/** Default machine-creds path hq-cloud also checks when the env is unset. */
|
|
260
|
+
export function defaultMachineCredsFile(home = os.homedir()) {
|
|
261
|
+
return path.join(home, ".hq-agent", "machine-creds.json");
|
|
262
|
+
}
|
|
259
263
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* never flips other CLI commands onto the machine path.
|
|
264
|
+
* Align `process.env.HQ_MACHINE_CREDS_FILE` with a (possibly synthetic) `env`
|
|
265
|
+
* for the duration of `fn`, so hq-cloud's `isMachineIdentity()` sees the same
|
|
266
|
+
* value doctor / tests intended.
|
|
264
267
|
*/
|
|
265
|
-
|
|
266
|
-
if (options.tokenSource === "person")
|
|
267
|
-
return false;
|
|
268
|
-
if (options.tokenSource === "machine") {
|
|
269
|
-
return isMachineIdentity();
|
|
270
|
-
}
|
|
271
|
-
if (!isMachineCredsFileEnvSet(env))
|
|
272
|
-
return false;
|
|
273
|
-
// Align process.env for hq-cloud's isMachineIdentity() when a synthetic env
|
|
274
|
-
// is supplied (doctor / tests).
|
|
268
|
+
function withAlignedMachineCredsEnv(env, fn) {
|
|
275
269
|
const prevCreds = process.env.HQ_MACHINE_CREDS_FILE;
|
|
276
270
|
const nextCreds = env.HQ_MACHINE_CREDS_FILE;
|
|
277
271
|
if (nextCreds !== prevCreds) {
|
|
278
|
-
if (nextCreds === undefined)
|
|
272
|
+
if (nextCreds === undefined || String(nextCreds).trim() === "") {
|
|
279
273
|
delete process.env.HQ_MACHINE_CREDS_FILE;
|
|
280
|
-
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
281
276
|
process.env.HQ_MACHINE_CREDS_FILE = nextCreds;
|
|
277
|
+
}
|
|
282
278
|
}
|
|
283
279
|
try {
|
|
284
|
-
return
|
|
280
|
+
return fn();
|
|
285
281
|
}
|
|
286
282
|
finally {
|
|
287
283
|
if (nextCreds !== prevCreds) {
|
|
@@ -293,9 +289,104 @@ export function wantsMachineCognitoTokens(options = {}, env = process.env) {
|
|
|
293
289
|
}
|
|
294
290
|
}
|
|
295
291
|
/**
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
292
|
+
* On-disk person Cognito cache path for `env` (`HQ_STATE_DIR` or `~/.hq`).
|
|
293
|
+
* Deliberately does not use hq-cloud's `loadCachedTokens()`: after a machine
|
|
294
|
+
* mint in this process, that helper returns in-memory machine tokens when the
|
|
295
|
+
* person file is absent, which would falsely block the fresh-box fallback.
|
|
296
|
+
*/
|
|
297
|
+
export function personTokenCacheFile(home = os.homedir(), env = process.env) {
|
|
298
|
+
const override = env.HQ_STATE_DIR?.trim();
|
|
299
|
+
const stateDir = override && override.length > 0 ? override : path.join(home, ".hq");
|
|
300
|
+
return path.join(stateDir, "cognito-tokens.json");
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* True when a usable person Cognito cache file exists under the effective
|
|
304
|
+
* `HQ_STATE_DIR` (or `~/.hq`). Presence alone keeps human commands on the
|
|
305
|
+
* person path even when the default machine-creds file is also on disk
|
|
306
|
+
* (no-flip guarantee).
|
|
307
|
+
*/
|
|
308
|
+
export function hasPersonCachedSession(env = process.env) {
|
|
309
|
+
try {
|
|
310
|
+
const file = personTokenCacheFile(os.homedir(), env);
|
|
311
|
+
if (!fs.existsSync(file))
|
|
312
|
+
return false;
|
|
313
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
314
|
+
if (typeof raw.accessToken !== "string" ||
|
|
315
|
+
raw.accessToken.length === 0 ||
|
|
316
|
+
typeof raw.idToken !== "string" ||
|
|
317
|
+
raw.idToken.length === 0) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
// Definitive refresh rejection writes `${file}.invalid.<fingerprint>`;
|
|
321
|
+
// treat that as no usable person session so status/whoami report machine.
|
|
322
|
+
const marker = `${file}.invalid.${accessTokenFingerprint(raw.accessToken)}`;
|
|
323
|
+
if (fs.existsSync(marker))
|
|
324
|
+
return false;
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Whether hq-cloud currently sees readable machine creds (env override or
|
|
333
|
+
* default `~/.hq-agent/machine-creds.json`), honoring a synthetic `env`.
|
|
334
|
+
*/
|
|
335
|
+
function probeMachineIdentity(env) {
|
|
336
|
+
return withAlignedMachineCredsEnv(env, () => isMachineIdentity());
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Fresh-box / Outpost bootstrap: env unset, not forced to person, no person
|
|
340
|
+
* cache, and the default machine-creds file is readable. Distinct from
|
|
341
|
+
* explicit `HQ_MACHINE_CREDS_FILE` / `{ tokenSource: "machine" }` opt-in.
|
|
342
|
+
*/
|
|
343
|
+
export function isDefaultMachineCredsFallback(options = {}, env = process.env) {
|
|
344
|
+
if (options.tokenSource === "person")
|
|
345
|
+
return false;
|
|
346
|
+
if (options.tokenSource === "machine")
|
|
347
|
+
return false;
|
|
348
|
+
if (isMachineCredsFileEnvSet(env))
|
|
349
|
+
return false;
|
|
350
|
+
if (hasPersonCachedSession(env))
|
|
351
|
+
return false;
|
|
352
|
+
return probeMachineIdentity(env);
|
|
353
|
+
}
|
|
354
|
+
function logDefaultMachineCredsFallback() {
|
|
355
|
+
console.error(chalk.dim("machine identity from default creds file"));
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Whether this call should mint/cache via machine creds (USER_PASSWORD_AUTH).
|
|
359
|
+
*
|
|
360
|
+
* Machine path when:
|
|
361
|
+
* - explicit `{ tokenSource: "machine" }` and creds are readable, or
|
|
362
|
+
* - `HQ_MACHINE_CREDS_FILE` is set and readable, or
|
|
363
|
+
* - fresh-box fallback: env unset, no person cache, default
|
|
364
|
+
* `~/.hq-agent/machine-creds.json` readable.
|
|
365
|
+
*
|
|
366
|
+
* Explicit `{ tokenSource: "person" }` never uses machine creds. A present
|
|
367
|
+
* person cache keeps the person path even when the default creds file exists
|
|
368
|
+
* (do not flip a human's commands onto machine tokens).
|
|
369
|
+
*/
|
|
370
|
+
export function wantsMachineCognitoTokens(options = {}, env = process.env) {
|
|
371
|
+
if (options.tokenSource === "person")
|
|
372
|
+
return false;
|
|
373
|
+
if (options.tokenSource === "machine") {
|
|
374
|
+
return probeMachineIdentity(env);
|
|
375
|
+
}
|
|
376
|
+
if (isMachineCredsFileEnvSet(env)) {
|
|
377
|
+
return probeMachineIdentity(env);
|
|
378
|
+
}
|
|
379
|
+
// Fresh-box fallback — default creds file only when no person session.
|
|
380
|
+
if (hasPersonCachedSession(env))
|
|
381
|
+
return false;
|
|
382
|
+
return probeMachineIdentity(env);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Which Cognito token source this process will use for doctor / status / whoami.
|
|
386
|
+
* Matches {@link wantsMachineCognitoTokens}: env opt-in, or default-file
|
|
387
|
+
* fallback when no usable person session is cached (absent, or marked rejected
|
|
388
|
+
* after a definitive Cognito refresh refusal). Transient refresh failures do
|
|
389
|
+
* not flip this to machine.
|
|
299
390
|
*/
|
|
300
391
|
export function resolveCognitoTokenSource(env = process.env) {
|
|
301
392
|
return wantsMachineCognitoTokens({}, env) ? "machine" : "person";
|
|
@@ -400,16 +491,13 @@ export function describeCognitoTokenSource(opts = {}) {
|
|
|
400
491
|
* Run `fn` with `HQ_STATE_DIR` pointed at the machine/daemon token cache so
|
|
401
492
|
* hq-cloud's mint writes never touch the person login file.
|
|
402
493
|
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
494
|
+
* Always redirects — even when the caller already set `HQ_STATE_DIR` (person
|
|
495
|
+
* cache / tests). Override the machine cache location with
|
|
496
|
+
* `HQ_MACHINE_TOKEN_STATE_DIR` instead. Restores the prior env afterwards.
|
|
405
497
|
*/
|
|
406
498
|
export async function withMachineTokenStateDir(fn, opts = {}) {
|
|
407
499
|
const home = opts.home ?? os.homedir();
|
|
408
500
|
const env = opts.env ?? process.env;
|
|
409
|
-
const existing = env.HQ_STATE_DIR?.trim() || process.env.HQ_STATE_DIR?.trim() || "";
|
|
410
|
-
if (existing) {
|
|
411
|
-
return fn();
|
|
412
|
-
}
|
|
413
501
|
const prev = process.env.HQ_STATE_DIR;
|
|
414
502
|
process.env.HQ_STATE_DIR = machineTokenStateDir(home, env);
|
|
415
503
|
try {
|
|
@@ -425,12 +513,54 @@ export async function withMachineTokenStateDir(fn, opts = {}) {
|
|
|
425
513
|
async function ensureMachineTokens() {
|
|
426
514
|
return withMachineTokenStateDir(() => getValidMachineTokens(DEFAULT_COGNITO));
|
|
427
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Mint machine tokens (daemon state dir cache). Logs once when the fresh-box
|
|
518
|
+
* default-creds fallback is what selected the machine path.
|
|
519
|
+
*/
|
|
520
|
+
async function ensureMachineTokensMaybeFallback(options = {}) {
|
|
521
|
+
if (isDefaultMachineCredsFallback(options)) {
|
|
522
|
+
logDefaultMachineCredsFallback();
|
|
523
|
+
}
|
|
524
|
+
return ensureMachineTokens();
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* After a person-cache miss or refresh failure: if the default machine-creds
|
|
528
|
+
* file is readable and the caller did not force person mode, mint from it.
|
|
529
|
+
* Returns null when the fallback does not apply.
|
|
530
|
+
*/
|
|
531
|
+
async function tryDefaultMachineCredsFallback(options = {}) {
|
|
532
|
+
if (options.tokenSource === "person")
|
|
533
|
+
return null;
|
|
534
|
+
if (isMachineCredsFileEnvSet())
|
|
535
|
+
return null;
|
|
536
|
+
if (!isMachineIdentity())
|
|
537
|
+
return null;
|
|
538
|
+
logDefaultMachineCredsFallback();
|
|
539
|
+
return ensureMachineTokens();
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* True when a person-token refresh failed with a definitive Cognito auth
|
|
543
|
+
* rejection (refresh token invalid/revoked). Transient network/5xx/timeouts
|
|
544
|
+
* must NOT trigger machine-identity fallback.
|
|
545
|
+
*/
|
|
546
|
+
export function isDefinitivePersonAuthRejection(err) {
|
|
547
|
+
if (err instanceof CognitoRefreshError) {
|
|
548
|
+
return err.requiresReauth === true;
|
|
549
|
+
}
|
|
550
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
551
|
+
return /NotAuthorizedException|invalid_grant|invalid refresh token/i.test(msg);
|
|
552
|
+
}
|
|
553
|
+
function markPersonSessionRejected(accessToken) {
|
|
554
|
+
invalidateCachedTokensByFingerprint(accessTokenFingerprint(accessToken));
|
|
555
|
+
}
|
|
428
556
|
/**
|
|
429
557
|
* Return a non-expired Cognito access token, refreshing or browser-logging-in
|
|
430
558
|
* as needed. Person cache lives at ~/.hq/cognito-tokens.json. Machine minting
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
559
|
+
* uses `HQ_MACHINE_CREDS_FILE` / `{ tokenSource: "machine" }`, or the fresh-box
|
|
560
|
+
* fallback when the default creds file is readable and no person session
|
|
561
|
+
* exists (or person refresh fails with a definitive auth rejection).
|
|
562
|
+
* Machine tokens always cache under the dedicated machine/daemon state dir
|
|
563
|
+
* (never the caller's `HQ_STATE_DIR` person cache).
|
|
434
564
|
*
|
|
435
565
|
* Pass `interactive: false` from automated contexts (e.g. the `hq-auth-refresh`
|
|
436
566
|
* bin invoked by the deploy skill) where failing fast is better than opening
|
|
@@ -445,10 +575,10 @@ export async function ensureCognitoToken(options = {}) {
|
|
|
445
575
|
// so vault-API calls from a machine identity send the ID token. The cached
|
|
446
576
|
// token file keeps correct field semantics (real access token in
|
|
447
577
|
// accessToken) for consumers that need token_use=access, e.g. the deploy
|
|
448
|
-
// API via the deploy skill. Cache path: daemon state dir
|
|
449
|
-
//
|
|
578
|
+
// API via the deploy skill. Cache path: dedicated machine/daemon state dir
|
|
579
|
+
// (isolated even when HQ_STATE_DIR points at a person cache).
|
|
450
580
|
if (wantsMachineCognitoTokens(options)) {
|
|
451
|
-
const machine = await
|
|
581
|
+
const machine = await ensureMachineTokensMaybeFallback(options);
|
|
452
582
|
return machine.idToken;
|
|
453
583
|
}
|
|
454
584
|
const cached = loadCachedTokens();
|
|
@@ -464,11 +594,24 @@ export async function ensureCognitoToken(options = {}) {
|
|
|
464
594
|
return refreshed.accessToken;
|
|
465
595
|
}
|
|
466
596
|
catch (err) {
|
|
597
|
+
// Transient/network/server errors stay person errors — never mint machine.
|
|
598
|
+
if (!isDefinitivePersonAuthRejection(err)) {
|
|
599
|
+
throw err;
|
|
600
|
+
}
|
|
601
|
+
markPersonSessionRejected(cached.accessToken);
|
|
602
|
+
const fallback = await tryDefaultMachineCredsFallback(options);
|
|
603
|
+
if (fallback)
|
|
604
|
+
return fallback.idToken;
|
|
467
605
|
if (interactive) {
|
|
468
606
|
console.error(chalk.dim(` Refresh failed (${err instanceof Error ? err.message : err}), falling back to browser login`));
|
|
469
607
|
}
|
|
470
608
|
}
|
|
471
609
|
}
|
|
610
|
+
else {
|
|
611
|
+
const fallback = await tryDefaultMachineCredsFallback(options);
|
|
612
|
+
if (fallback)
|
|
613
|
+
return fallback.idToken;
|
|
614
|
+
}
|
|
472
615
|
if (!interactive) {
|
|
473
616
|
throw new Error("No valid HQ session and interactive login is disabled. Run `hq login` first.");
|
|
474
617
|
}
|
|
@@ -490,7 +633,7 @@ export async function ensureCognitoToken(options = {}) {
|
|
|
490
633
|
export async function ensureCognitoIdToken(options = {}) {
|
|
491
634
|
const interactive = options.interactive ?? true;
|
|
492
635
|
if (wantsMachineCognitoTokens(options)) {
|
|
493
|
-
const machine = await
|
|
636
|
+
const machine = await ensureMachineTokensMaybeFallback(options);
|
|
494
637
|
return machine.idToken;
|
|
495
638
|
}
|
|
496
639
|
const cached = loadCachedTokens();
|
|
@@ -506,11 +649,23 @@ export async function ensureCognitoIdToken(options = {}) {
|
|
|
506
649
|
return refreshed.idToken;
|
|
507
650
|
}
|
|
508
651
|
catch (err) {
|
|
652
|
+
if (!isDefinitivePersonAuthRejection(err)) {
|
|
653
|
+
throw err;
|
|
654
|
+
}
|
|
655
|
+
markPersonSessionRejected(cached.accessToken);
|
|
656
|
+
const fallback = await tryDefaultMachineCredsFallback(options);
|
|
657
|
+
if (fallback)
|
|
658
|
+
return fallback.idToken;
|
|
509
659
|
if (interactive) {
|
|
510
660
|
console.error(chalk.dim(` Refresh failed (${err instanceof Error ? err.message : err}), falling back to browser login`));
|
|
511
661
|
}
|
|
512
662
|
}
|
|
513
663
|
}
|
|
664
|
+
else {
|
|
665
|
+
const fallback = await tryDefaultMachineCredsFallback(options);
|
|
666
|
+
if (fallback)
|
|
667
|
+
return fallback.idToken;
|
|
668
|
+
}
|
|
514
669
|
if (!interactive) {
|
|
515
670
|
throw new Error("No valid HQ session and interactive login is disabled. Run `hq login` first.");
|
|
516
671
|
}
|
|
@@ -551,10 +706,11 @@ export function buildVaultConfig(authToken) {
|
|
|
551
706
|
export async function refreshCachedSession(options = {}) {
|
|
552
707
|
// Machine identities have no refresh token; ensure a valid cached machine
|
|
553
708
|
// session without forcing a re-mint when the cache is already healthy.
|
|
554
|
-
// Tokens land under the daemon state dir
|
|
709
|
+
// Tokens land under the dedicated machine/daemon state dir.
|
|
710
|
+
// Includes the fresh-box default-creds fallback when no person session.
|
|
555
711
|
if (wantsMachineCognitoTokens(options)) {
|
|
556
712
|
try {
|
|
557
|
-
await
|
|
713
|
+
await ensureMachineTokensMaybeFallback(options);
|
|
558
714
|
return { refreshed: true };
|
|
559
715
|
}
|
|
560
716
|
catch (err) {
|
|
@@ -566,6 +722,20 @@ export async function refreshCachedSession(options = {}) {
|
|
|
566
722
|
}
|
|
567
723
|
const cached = loadCachedTokens();
|
|
568
724
|
if (!cached) {
|
|
725
|
+
// Person path had nothing; last chance is default machine creds (e.g. the
|
|
726
|
+
// cache was cleared between the wantsMachine check and now, or identity
|
|
727
|
+
// became readable). Prefer machine mint over "no cached session" on boxes.
|
|
728
|
+
try {
|
|
729
|
+
const fallback = await tryDefaultMachineCredsFallback(options);
|
|
730
|
+
if (fallback)
|
|
731
|
+
return { refreshed: true };
|
|
732
|
+
}
|
|
733
|
+
catch (err) {
|
|
734
|
+
return {
|
|
735
|
+
refreshed: false,
|
|
736
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
737
|
+
};
|
|
738
|
+
}
|
|
569
739
|
return { refreshed: false, reason: "no cached session" };
|
|
570
740
|
}
|
|
571
741
|
if (!isExpiring(cached, 120)) {
|
|
@@ -576,6 +746,26 @@ export async function refreshCachedSession(options = {}) {
|
|
|
576
746
|
return { refreshed: true };
|
|
577
747
|
}
|
|
578
748
|
catch (err) {
|
|
749
|
+
// Transient/network/server refresh errors stay person failures.
|
|
750
|
+
if (!isDefinitivePersonAuthRejection(err)) {
|
|
751
|
+
return {
|
|
752
|
+
refreshed: false,
|
|
753
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
// Definitive rejection — person session is dead; mark it and fall back.
|
|
757
|
+
markPersonSessionRejected(cached.accessToken);
|
|
758
|
+
try {
|
|
759
|
+
const fallback = await tryDefaultMachineCredsFallback(options);
|
|
760
|
+
if (fallback)
|
|
761
|
+
return { refreshed: true };
|
|
762
|
+
}
|
|
763
|
+
catch (mintErr) {
|
|
764
|
+
return {
|
|
765
|
+
refreshed: false,
|
|
766
|
+
reason: mintErr instanceof Error ? mintErr.message : String(mintErr),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
579
769
|
return {
|
|
580
770
|
refreshed: false,
|
|
581
771
|
reason: err instanceof Error ? err.message : String(err),
|