@indigoai-us/hq-cli 5.345.105 → 5.345.107
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 +23 -0
- package/assets/scaffold/core/scripts/hq-task.mjs +259 -0
- package/assets/scaffold/core/scripts/hq-task.sh +31 -0
- package/assets/scaffold/core/scripts/pipeline-worker-table.sh +285 -0
- package/dist/command-catalog.generated.d.ts +135 -5
- package/dist/command-catalog.generated.js +161 -5
- package/dist/commands/auth.js +42 -0
- package/dist/commands/core-hq-task-fast.d.ts +4 -0
- package/dist/commands/core-hq-task-fast.js +11 -0
- package/dist/commands/core-hq-task-native.d.ts +20 -0
- package/dist/commands/core-hq-task-native.js +275 -0
- package/dist/commands/core-native-bucket-c-flag.d.ts +3 -0
- package/dist/commands/core-native-bucket-c-flag.js +6 -0
- package/dist/commands/core-native-family-flag.d.ts +0 -2
- package/dist/commands/core-native-family-flag.js +0 -4
- package/dist/commands/core-native-pipeline-flag.d.ts +3 -0
- package/dist/commands/core-native-pipeline-flag.js +6 -0
- package/dist/commands/core-ontology-native.js +24 -30
- package/dist/commands/core-pipeline-workers-command.d.ts +3 -0
- package/dist/commands/core-pipeline-workers-command.js +37 -0
- package/dist/commands/core-pipeline-workers-native.d.ts +13 -0
- package/dist/commands/core-pipeline-workers-native.js +361 -0
- package/dist/commands/core.js +12 -10
- package/dist/commands/lanes.js +32 -7
- package/dist/commands/logout.d.ts +6 -1
- package/dist/commands/logout.js +18 -2
- package/dist/commands/outposts.d.ts +5 -0
- package/dist/commands/outposts.js +5 -0
- package/dist/fast-scaffold.d.ts +14 -1
- package/dist/fast-scaffold.js +149 -5
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +56937 -64550
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +15 -1
- package/dist/lib/daemon/remote-control-pools.d.ts +2 -2
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.d.ts +33 -0
- package/dist/lib/lanes/remote-control-lane.js +276 -0
- package/dist/lib/lanes/spawn.d.ts +5 -0
- package/dist/lib/lanes/spawn.js +65 -14
- package/dist/lib/lanes/store.js +7 -0
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.js +8 -1
- package/dist/utils/cognito-config.d.ts +5 -0
- package/dist/utils/cognito-config.js +14 -0
- package/dist/utils/cognito-revoke.d.ts +33 -0
- package/dist/utils/cognito-revoke.js +42 -0
- package/dist/utils/cognito-session.d.ts +12 -4
- package/dist/utils/cognito-session.js +42 -9
- package/dist/utils/token-keychain.d.ts +96 -0
- package/dist/utils/token-keychain.js +218 -0
- package/package.json +2 -2
- package/assets/scaffold/core/scripts/ontology-audience-grant.sh +0 -81
- package/assets/scaffold/core/scripts/ontology-brief.mjs +0 -93
- package/assets/scaffold/core/scripts/ontology-candidate.sh +0 -100
- package/assets/scaffold/core/scripts/ontology-garden.mjs +0 -327
- package/assets/scaffold/core/scripts/ontology-source-worklist.sh +0 -226
- package/assets/scaffold/core/scripts/source-yaml-validate.sh +0 -36
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side revocation of a Cognito refresh token.
|
|
3
|
+
*
|
|
4
|
+
* `hq logout` calls this before it deletes the local token file so that a
|
|
5
|
+
* copied token file stops working too. Cognito's `/oauth2/revoke` endpoint on
|
|
6
|
+
* the hosted domain (the same domain the CLI already uses for `/oauth2/token`)
|
|
7
|
+
* revokes the refresh token and every access/ID token issued from it. The CLI
|
|
8
|
+
* app client is public (no client secret), so the body carries only the token
|
|
9
|
+
* and the client id.
|
|
10
|
+
*
|
|
11
|
+
* This never throws and never includes the token in its result: callers get a
|
|
12
|
+
* plain outcome they can print.
|
|
13
|
+
*/
|
|
14
|
+
/** Upper bound on the revoke request so logout never hangs on a dead network. */
|
|
15
|
+
export const REVOKE_TIMEOUT_MS = 5_000;
|
|
16
|
+
export function cognitoRevokeEndpoint(config) {
|
|
17
|
+
return `https://${config.userPoolDomain}.auth.${config.region}.amazoncognito.com/oauth2/revoke`;
|
|
18
|
+
}
|
|
19
|
+
export async function revokeRefreshToken(refreshToken, config, opts = {}) {
|
|
20
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
21
|
+
const body = new URLSearchParams({ token: refreshToken, client_id: config.clientId });
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetchImpl(cognitoRevokeEndpoint(config), {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
26
|
+
body: body.toString(),
|
|
27
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? REVOKE_TIMEOUT_MS),
|
|
28
|
+
});
|
|
29
|
+
// Drain the body so the socket is released; its content is not needed.
|
|
30
|
+
await response.text().catch(() => undefined);
|
|
31
|
+
if (response.ok)
|
|
32
|
+
return { ok: true };
|
|
33
|
+
return { ok: false, reason: "rejected", status: response.status };
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const name = error instanceof Error ? error.name : "";
|
|
37
|
+
if (name === "TimeoutError" || name === "AbortError")
|
|
38
|
+
return { ok: false, reason: "timeout" };
|
|
39
|
+
return { ok: false, reason: "network" };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=cognito-revoke.js.map
|
|
@@ -18,8 +18,9 @@
|
|
|
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 {
|
|
22
|
-
|
|
21
|
+
import { isExpiring, isMachineIdentity, type CognitoAuthConfig, type CognitoTokens, type ClientInfo, type VaultServiceConfig } from "@indigoai-us/hq-cloud";
|
|
22
|
+
import { type KeychainReadOptions } from "./token-keychain.js";
|
|
23
|
+
export { isExpiring, isMachineIdentity };
|
|
23
24
|
/** Person browser/login cache vs machine-creds USER_PASSWORD_AUTH mint. */
|
|
24
25
|
export type CognitoTokenSource = "person" | "machine";
|
|
25
26
|
/** Actor kind derived from the active token / machine-creds signal. */
|
|
@@ -186,11 +187,18 @@ export declare function formatCognitoSubject(tokens: {
|
|
|
186
187
|
accessToken?: string;
|
|
187
188
|
} | null | undefined): string;
|
|
188
189
|
/**
|
|
189
|
-
* Person
|
|
190
|
+
* Person session for `env`: the Keychain item and the token file, the newer
|
|
191
|
+
* by `expiresAt` (see ./token-keychain.ts). Does not consult hq-cloud's
|
|
190
192
|
* in-memory fallback, which can surface a different session when the file
|
|
191
193
|
* is missing or unreadable.
|
|
192
194
|
*/
|
|
193
|
-
export declare function readPersonTokenFile(home?: string, env?: NodeJS.ProcessEnv): CognitoTokens | null;
|
|
195
|
+
export declare function readPersonTokenFile(home?: string, env?: NodeJS.ProcessEnv, keychain?: KeychainReadOptions): CognitoTokens | null;
|
|
196
|
+
/**
|
|
197
|
+
* The person's cached session: the shared Keychain item and hq-cloud's file
|
|
198
|
+
* loader, the newer by `expiresAt`. Writes still go only to the file in this
|
|
199
|
+
* step.
|
|
200
|
+
*/
|
|
201
|
+
export declare function loadCachedTokens(env?: NodeJS.ProcessEnv, keychain?: KeychainReadOptions): CognitoTokens | null;
|
|
194
202
|
/**
|
|
195
203
|
* Refresh the active person session and refuse a subject change.
|
|
196
204
|
*
|
|
@@ -25,15 +25,16 @@ import * as os from "os";
|
|
|
25
25
|
import * as path from "path";
|
|
26
26
|
import * as yaml from "js-yaml";
|
|
27
27
|
import chalk from "chalk";
|
|
28
|
-
import { loadCachedTokens, isExpiring, refreshTokens, saveCachedTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, CognitoRefreshError, accessTokenFingerprint, invalidateCachedTokensByFingerprint, } from "@indigoai-us/hq-cloud";
|
|
28
|
+
import { loadCachedTokens as loadCachedTokensFromFile, isExpiring, refreshTokens, saveCachedTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, CognitoRefreshError, accessTokenFingerprint, invalidateCachedTokensByFingerprint, } from "@indigoai-us/hq-cloud";
|
|
29
29
|
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
30
30
|
import { daemonDir } from "../lib/mesh/live/daemon/paths.js";
|
|
31
31
|
import { workMeshRoot } from "../lib/mesh/live/paths.js";
|
|
32
32
|
import { acquireLockFileExclusively } from "./exclusive-lock.js";
|
|
33
33
|
import { reportNoninteractivePersonRefreshRejection } from "./cognito-refresh-sentry.js";
|
|
34
|
+
import { pickNewerSession, readKeychainTokens } from "./token-keychain.js";
|
|
34
35
|
// Re-export the cached-session primitives for callers that must deliberately
|
|
35
36
|
// avoid the refresh/login behavior in the higher-level helpers below.
|
|
36
|
-
export { isExpiring, isMachineIdentity
|
|
37
|
+
export { isExpiring, isMachineIdentity };
|
|
37
38
|
/**
|
|
38
39
|
* Override for where machine-minted Cognito tokens are cached.
|
|
39
40
|
* Default: the work-mesh daemon state dir (`~/.hq/work-mesh/daemon`), so a
|
|
@@ -313,10 +314,9 @@ export function personTokenCacheFile(home = os.homedir(), env = process.env) {
|
|
|
313
314
|
export function hasPersonCachedSession(env = process.env) {
|
|
314
315
|
try {
|
|
315
316
|
const file = personTokenCacheFile(os.homedir(), env);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (typeof raw.accessToken !== "string" ||
|
|
317
|
+
const raw = readPersonTokenFile(os.homedir(), env);
|
|
318
|
+
if (!raw ||
|
|
319
|
+
typeof raw.accessToken !== "string" ||
|
|
320
320
|
raw.accessToken.length === 0 ||
|
|
321
321
|
typeof raw.idToken !== "string" ||
|
|
322
322
|
raw.idToken.length === 0) {
|
|
@@ -474,12 +474,45 @@ function readTokenFile(file) {
|
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
476
|
/**
|
|
477
|
-
*
|
|
477
|
+
* The person session from the shared macOS Keychain item (see
|
|
478
|
+
* ./token-keychain.ts), or null. Not read when the token file is missing:
|
|
479
|
+
* no file means signed out. Machine-identity and invalidated tokens are
|
|
480
|
+
* refused, the same as the file loader.
|
|
481
|
+
*/
|
|
482
|
+
function personKeychainTokens(file, home, env, keychain = {}) {
|
|
483
|
+
if (!fs.existsSync(file))
|
|
484
|
+
return null;
|
|
485
|
+
const tokens = readKeychainTokens({ env, home, ...keychain });
|
|
486
|
+
if (!tokens)
|
|
487
|
+
return null;
|
|
488
|
+
const claims = decodeJwtPayload(tokens.idToken);
|
|
489
|
+
const entityType = claims?.["custom:entityType"];
|
|
490
|
+
if (typeof entityType === "string" && entityType.trim().length > 0)
|
|
491
|
+
return null;
|
|
492
|
+
if (fs.existsSync(`${file}.invalid.${accessTokenFingerprint(tokens.accessToken)}`))
|
|
493
|
+
return null;
|
|
494
|
+
return tokens;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Person session for `env`: the Keychain item and the token file, the newer
|
|
498
|
+
* by `expiresAt` (see ./token-keychain.ts). Does not consult hq-cloud's
|
|
478
499
|
* in-memory fallback, which can surface a different session when the file
|
|
479
500
|
* is missing or unreadable.
|
|
480
501
|
*/
|
|
481
|
-
export function readPersonTokenFile(home = os.homedir(), env = process.env) {
|
|
482
|
-
|
|
502
|
+
export function readPersonTokenFile(home = os.homedir(), env = process.env, keychain = {}) {
|
|
503
|
+
const file = personTokenCacheFile(home, env);
|
|
504
|
+
return pickNewerSession(personKeychainTokens(file, home, env, keychain), readTokenFile(file));
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* The person's cached session: the shared Keychain item and hq-cloud's file
|
|
508
|
+
* loader, the newer by `expiresAt`. Writes still go only to the file in this
|
|
509
|
+
* step.
|
|
510
|
+
*/
|
|
511
|
+
export function loadCachedTokens(env = process.env, keychain = {}) {
|
|
512
|
+
const home = os.homedir();
|
|
513
|
+
const file = personTokenCacheFile(home, env);
|
|
514
|
+
const fromFile = loadCachedTokensFromFile();
|
|
515
|
+
return pickNewerSession(personKeychainTokens(file, home, env, keychain), fromFile);
|
|
483
516
|
}
|
|
484
517
|
function restoreTokenFile(file, before) {
|
|
485
518
|
if (before) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS Keychain read of the person Cognito session (step 1 of moving the
|
|
3
|
+
* session off `~/.hq/cognito-tokens.json`).
|
|
4
|
+
*
|
|
5
|
+
* Shared item (must match hq-desktop-app `token_keychain.rs`, desktop #1586,
|
|
6
|
+
* and hq-cloud `src/token-keychain.ts`):
|
|
7
|
+
* - class: generic password, login keychain
|
|
8
|
+
* - service: `ai.indigo.hq.cognito-tokens`
|
|
9
|
+
* - account: `person`
|
|
10
|
+
* - value: the same camelCase JSON as the token file
|
|
11
|
+
* - signer: Team ID HWFZ2QDTMR (login-keychain partition `teamid:HWFZ2QDTMR`)
|
|
12
|
+
*
|
|
13
|
+
* The read goes through `/usr/bin/security`, so there is no native module.
|
|
14
|
+
* macOS asks the user once whether `security` may read the item; HQ accepts
|
|
15
|
+
* that prompt (owner decision) and explains it first:
|
|
16
|
+
* - Before the first read, a one-line note is printed to stderr. It is only
|
|
17
|
+
* shown, and the first read only happens, in an interactive terminal, so
|
|
18
|
+
* a background process never raises the prompt unexplained.
|
|
19
|
+
* - A denied prompt, a timeout or any other failure falls back to the file
|
|
20
|
+
* quietly and is remembered in `~/.hq/keychain-access.json`, so HQ does
|
|
21
|
+
* not ask again on every run. `hq auth keychain --retry` clears it.
|
|
22
|
+
* - "Item not found" is a plain miss and is not remembered as a denial.
|
|
23
|
+
*
|
|
24
|
+
* Choosing between the Keychain and the file (writes still go to the file):
|
|
25
|
+
* - No file means signed out; the Keychain is not read. Older tools sign
|
|
26
|
+
* out by deleting only the file, and the desktop clears the stale item.
|
|
27
|
+
* - Both valid: the newer session by `expiresAt` wins; a tie keeps the
|
|
28
|
+
* Keychain value. This matches the installer's rule.
|
|
29
|
+
* - Only one valid: that one.
|
|
30
|
+
*
|
|
31
|
+
* Non-macOS, an `HQ_STATE_DIR` override and `HQ_TOKEN_KEYCHAIN=0` read the
|
|
32
|
+
* file only. Token values are never printed or logged.
|
|
33
|
+
*/
|
|
34
|
+
export declare const KEYCHAIN_SERVICE = "ai.indigo.hq.cognito-tokens";
|
|
35
|
+
export declare const KEYCHAIN_ACCOUNT = "person";
|
|
36
|
+
export declare const KEYCHAIN_TEAM_ID = "HWFZ2QDTMR";
|
|
37
|
+
/** Read timeout once access has been allowed (no prompt expected). */
|
|
38
|
+
export declare const KEYCHAIN_READ_TIMEOUT_MS = 3000;
|
|
39
|
+
/** First read: long enough for a person to answer the macOS prompt. */
|
|
40
|
+
export declare const KEYCHAIN_FIRST_READ_TIMEOUT_MS = 60000;
|
|
41
|
+
export declare const KEYCHAIN_FIRST_READ_NOTE = "macOS will ask once whether HQ can read your sign-in from the Keychain. Choose Always Allow.";
|
|
42
|
+
export interface KeychainTokenShape {
|
|
43
|
+
accessToken: string;
|
|
44
|
+
idToken: string;
|
|
45
|
+
refreshToken?: string;
|
|
46
|
+
expiresAt?: string | number;
|
|
47
|
+
}
|
|
48
|
+
export interface SecurityResult {
|
|
49
|
+
status: number | null;
|
|
50
|
+
stdout: string;
|
|
51
|
+
timedOut: boolean;
|
|
52
|
+
}
|
|
53
|
+
export type SecurityRunner = (args: string[], timeoutMs: number) => SecurityResult;
|
|
54
|
+
export interface KeychainAccessState {
|
|
55
|
+
noteShownAt?: string;
|
|
56
|
+
allowedAt?: string;
|
|
57
|
+
deniedAt?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface KeychainReadOptions {
|
|
60
|
+
platform?: NodeJS.Platform;
|
|
61
|
+
env?: NodeJS.ProcessEnv;
|
|
62
|
+
home?: string;
|
|
63
|
+
runSecurity?: SecurityRunner;
|
|
64
|
+
/** Defaults to whether stderr is a terminal. */
|
|
65
|
+
interactive?: boolean;
|
|
66
|
+
/** Where the first-read note goes. Defaults to stderr. */
|
|
67
|
+
notify?: (line: string) => void;
|
|
68
|
+
/** Ignore a remembered denial and the read cache (`hq auth keychain --retry`). */
|
|
69
|
+
retry?: boolean;
|
|
70
|
+
now?: () => Date;
|
|
71
|
+
}
|
|
72
|
+
export type KeychainReadOutcome = "hit" | "miss" | "denied" | "skipped-denied" | "skipped-not-interactive" | "disabled";
|
|
73
|
+
export declare function keychainTokenReadEnabled(opts?: KeychainReadOptions): boolean;
|
|
74
|
+
export declare function keychainAccessStateFile(home?: string): string;
|
|
75
|
+
export declare function readKeychainAccessState(home?: string): KeychainAccessState;
|
|
76
|
+
export declare function parseTokenJson(raw: string): KeychainTokenShape | null;
|
|
77
|
+
/** Test seam: forget the in-process read cache. */
|
|
78
|
+
export declare function resetKeychainReadCacheForTests(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Read the shared item, applying the first-read note, the remembered denial
|
|
81
|
+
* and the timeouts described above. Never throws.
|
|
82
|
+
*/
|
|
83
|
+
export declare function readKeychainTokensDetailed(opts?: KeychainReadOptions): {
|
|
84
|
+
tokens: KeychainTokenShape | null;
|
|
85
|
+
outcome: KeychainReadOutcome;
|
|
86
|
+
};
|
|
87
|
+
export declare function readKeychainTokens(opts?: KeychainReadOptions): KeychainTokenShape | null;
|
|
88
|
+
/** Newer session by `expiresAt`; a tie keeps the Keychain value. */
|
|
89
|
+
export declare function pickNewerSession<T extends KeychainTokenShape>(fromKeychain: T | null, fromFile: T | null): T | null;
|
|
90
|
+
export type PersonTokenSource = "file" | "keychain" | "none";
|
|
91
|
+
/** The person session from the Keychain and the file, by the rules above. */
|
|
92
|
+
export declare function readPersonTokensWithKeychain(file?: string, opts?: KeychainReadOptions): {
|
|
93
|
+
tokens: KeychainTokenShape | null;
|
|
94
|
+
source: PersonTokenSource;
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=token-keychain.d.ts.map
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS Keychain read of the person Cognito session (step 1 of moving the
|
|
3
|
+
* session off `~/.hq/cognito-tokens.json`).
|
|
4
|
+
*
|
|
5
|
+
* Shared item (must match hq-desktop-app `token_keychain.rs`, desktop #1586,
|
|
6
|
+
* and hq-cloud `src/token-keychain.ts`):
|
|
7
|
+
* - class: generic password, login keychain
|
|
8
|
+
* - service: `ai.indigo.hq.cognito-tokens`
|
|
9
|
+
* - account: `person`
|
|
10
|
+
* - value: the same camelCase JSON as the token file
|
|
11
|
+
* - signer: Team ID HWFZ2QDTMR (login-keychain partition `teamid:HWFZ2QDTMR`)
|
|
12
|
+
*
|
|
13
|
+
* The read goes through `/usr/bin/security`, so there is no native module.
|
|
14
|
+
* macOS asks the user once whether `security` may read the item; HQ accepts
|
|
15
|
+
* that prompt (owner decision) and explains it first:
|
|
16
|
+
* - Before the first read, a one-line note is printed to stderr. It is only
|
|
17
|
+
* shown, and the first read only happens, in an interactive terminal, so
|
|
18
|
+
* a background process never raises the prompt unexplained.
|
|
19
|
+
* - A denied prompt, a timeout or any other failure falls back to the file
|
|
20
|
+
* quietly and is remembered in `~/.hq/keychain-access.json`, so HQ does
|
|
21
|
+
* not ask again on every run. `hq auth keychain --retry` clears it.
|
|
22
|
+
* - "Item not found" is a plain miss and is not remembered as a denial.
|
|
23
|
+
*
|
|
24
|
+
* Choosing between the Keychain and the file (writes still go to the file):
|
|
25
|
+
* - No file means signed out; the Keychain is not read. Older tools sign
|
|
26
|
+
* out by deleting only the file, and the desktop clears the stale item.
|
|
27
|
+
* - Both valid: the newer session by `expiresAt` wins; a tie keeps the
|
|
28
|
+
* Keychain value. This matches the installer's rule.
|
|
29
|
+
* - Only one valid: that one.
|
|
30
|
+
*
|
|
31
|
+
* Non-macOS, an `HQ_STATE_DIR` override and `HQ_TOKEN_KEYCHAIN=0` read the
|
|
32
|
+
* file only. Token values are never printed or logged.
|
|
33
|
+
*/
|
|
34
|
+
import { spawnSync } from "node:child_process";
|
|
35
|
+
import * as fs from "node:fs";
|
|
36
|
+
import * as os from "node:os";
|
|
37
|
+
import * as path from "node:path";
|
|
38
|
+
export const KEYCHAIN_SERVICE = "ai.indigo.hq.cognito-tokens";
|
|
39
|
+
export const KEYCHAIN_ACCOUNT = "person";
|
|
40
|
+
export const KEYCHAIN_TEAM_ID = "HWFZ2QDTMR";
|
|
41
|
+
/** Read timeout once access has been allowed (no prompt expected). */
|
|
42
|
+
export const KEYCHAIN_READ_TIMEOUT_MS = 3_000;
|
|
43
|
+
/** First read: long enough for a person to answer the macOS prompt. */
|
|
44
|
+
export const KEYCHAIN_FIRST_READ_TIMEOUT_MS = 60_000;
|
|
45
|
+
/** `security` exit status for errSecItemNotFound. */
|
|
46
|
+
const SECURITY_ITEM_NOT_FOUND = 44;
|
|
47
|
+
/** Reuse a read within one process for this long (avoids a spawn per request). */
|
|
48
|
+
const READ_CACHE_MS = 30_000;
|
|
49
|
+
export const KEYCHAIN_FIRST_READ_NOTE = "macOS will ask once whether HQ can read your sign-in from the Keychain. Choose Always Allow.";
|
|
50
|
+
const defaultRunner = (args, timeoutMs) => {
|
|
51
|
+
try {
|
|
52
|
+
const result = spawnSync("/usr/bin/security", args, {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
timeout: timeoutMs,
|
|
55
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
56
|
+
});
|
|
57
|
+
const timedOut = result.error?.code === "ETIMEDOUT";
|
|
58
|
+
return { status: result.status, stdout: typeof result.stdout === "string" ? result.stdout : "", timedOut };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { status: null, stdout: "", timedOut: false };
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
export function keychainTokenReadEnabled(opts = {}) {
|
|
65
|
+
const platform = opts.platform ?? process.platform;
|
|
66
|
+
const env = opts.env ?? process.env;
|
|
67
|
+
if (platform !== "darwin")
|
|
68
|
+
return false;
|
|
69
|
+
const flag = env.HQ_TOKEN_KEYCHAIN?.trim().toLowerCase();
|
|
70
|
+
if (flag === "0" || flag === "false" || flag === "off")
|
|
71
|
+
return false;
|
|
72
|
+
// Only the default ~/.hq cache is mirrored into the shared item.
|
|
73
|
+
if (env.HQ_STATE_DIR?.trim())
|
|
74
|
+
return false;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
export function keychainAccessStateFile(home = os.homedir()) {
|
|
78
|
+
return path.join(home, ".hq", "keychain-access.json");
|
|
79
|
+
}
|
|
80
|
+
export function readKeychainAccessState(home = os.homedir()) {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(fs.readFileSync(keychainAccessStateFile(home), "utf8"));
|
|
83
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function writeKeychainAccessState(home, state) {
|
|
90
|
+
try {
|
|
91
|
+
const file = keychainAccessStateFile(home);
|
|
92
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
93
|
+
const tmp = `${file}.tmp.${process.pid}`;
|
|
94
|
+
fs.writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
95
|
+
fs.renameSync(tmp, file);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Best effort: without the marker HQ may ask again, which is safe.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** `security -w` prints hex when the secret has non-printable bytes (newlines). */
|
|
102
|
+
function decodeSecurityOutput(stdout) {
|
|
103
|
+
const trimmed = stdout.replace(/\r?\n$/, "");
|
|
104
|
+
if (!trimmed.startsWith("{") && /^(?:[0-9a-fA-F]{2})+$/.test(trimmed)) {
|
|
105
|
+
return Buffer.from(trimmed, "hex").toString("utf8");
|
|
106
|
+
}
|
|
107
|
+
return trimmed;
|
|
108
|
+
}
|
|
109
|
+
export function parseTokenJson(raw) {
|
|
110
|
+
try {
|
|
111
|
+
const value = JSON.parse(raw);
|
|
112
|
+
if (!value ||
|
|
113
|
+
typeof value.accessToken !== "string" ||
|
|
114
|
+
value.accessToken.length === 0 ||
|
|
115
|
+
typeof value.idToken !== "string" ||
|
|
116
|
+
value.idToken.length === 0) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let readCache = null;
|
|
126
|
+
/** Test seam: forget the in-process read cache. */
|
|
127
|
+
export function resetKeychainReadCacheForTests() {
|
|
128
|
+
readCache = null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Read the shared item, applying the first-read note, the remembered denial
|
|
132
|
+
* and the timeouts described above. Never throws.
|
|
133
|
+
*/
|
|
134
|
+
export function readKeychainTokensDetailed(opts = {}) {
|
|
135
|
+
if (!keychainTokenReadEnabled(opts))
|
|
136
|
+
return { tokens: null, outcome: "disabled" };
|
|
137
|
+
const home = opts.home ?? os.homedir();
|
|
138
|
+
const now = opts.now ?? (() => new Date());
|
|
139
|
+
if (!opts.retry && readCache && readCache.home === home && now().getTime() - readCache.at < READ_CACHE_MS) {
|
|
140
|
+
return { tokens: readCache.tokens, outcome: readCache.outcome };
|
|
141
|
+
}
|
|
142
|
+
const remember = (tokens, outcome) => {
|
|
143
|
+
readCache = { at: now().getTime(), home, tokens, outcome };
|
|
144
|
+
return { tokens, outcome };
|
|
145
|
+
};
|
|
146
|
+
let state = readKeychainAccessState(home);
|
|
147
|
+
if (opts.retry && state.deniedAt) {
|
|
148
|
+
const { deniedAt: _cleared, ...rest } = state;
|
|
149
|
+
state = rest;
|
|
150
|
+
writeKeychainAccessState(home, state);
|
|
151
|
+
}
|
|
152
|
+
if (state.deniedAt)
|
|
153
|
+
return remember(null, "skipped-denied");
|
|
154
|
+
// Until a read has succeeded, a read may raise the macOS prompt, so it only
|
|
155
|
+
// happens in a terminal, after the note (shown once).
|
|
156
|
+
if (!state.allowedAt) {
|
|
157
|
+
const interactive = opts.interactive ?? Boolean(process.stderr.isTTY);
|
|
158
|
+
if (!interactive)
|
|
159
|
+
return remember(null, "skipped-not-interactive");
|
|
160
|
+
if (!state.noteShownAt) {
|
|
161
|
+
(opts.notify ?? ((line) => process.stderr.write(`${line}\n`)))(KEYCHAIN_FIRST_READ_NOTE);
|
|
162
|
+
state = { ...state, noteShownAt: now().toISOString() };
|
|
163
|
+
writeKeychainAccessState(home, state);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const run = opts.runSecurity ?? defaultRunner;
|
|
167
|
+
const result = run(["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w"], state.allowedAt ? KEYCHAIN_READ_TIMEOUT_MS : KEYCHAIN_FIRST_READ_TIMEOUT_MS);
|
|
168
|
+
if (result.status === 0 && !result.timedOut) {
|
|
169
|
+
if (!state.allowedAt)
|
|
170
|
+
writeKeychainAccessState(home, { ...state, allowedAt: now().toISOString() });
|
|
171
|
+
return remember(parseTokenJson(decodeSecurityOutput(result.stdout)), "hit");
|
|
172
|
+
}
|
|
173
|
+
if (result.status === SECURITY_ITEM_NOT_FOUND && !result.timedOut)
|
|
174
|
+
return remember(null, "miss");
|
|
175
|
+
// Denied, timed out, or failed: fall back quietly and don't ask again.
|
|
176
|
+
writeKeychainAccessState(home, { ...state, deniedAt: now().toISOString() });
|
|
177
|
+
return remember(null, "denied");
|
|
178
|
+
}
|
|
179
|
+
export function readKeychainTokens(opts = {}) {
|
|
180
|
+
return readKeychainTokensDetailed(opts).tokens;
|
|
181
|
+
}
|
|
182
|
+
function expiresAtMs(tokens) {
|
|
183
|
+
const value = tokens.expiresAt;
|
|
184
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
185
|
+
return value;
|
|
186
|
+
if (typeof value === "string") {
|
|
187
|
+
const parsed = Date.parse(value);
|
|
188
|
+
if (Number.isFinite(parsed))
|
|
189
|
+
return parsed;
|
|
190
|
+
}
|
|
191
|
+
return Number.NEGATIVE_INFINITY;
|
|
192
|
+
}
|
|
193
|
+
/** Newer session by `expiresAt`; a tie keeps the Keychain value. */
|
|
194
|
+
export function pickNewerSession(fromKeychain, fromFile) {
|
|
195
|
+
if (!fromKeychain)
|
|
196
|
+
return fromFile;
|
|
197
|
+
if (!fromFile)
|
|
198
|
+
return fromKeychain;
|
|
199
|
+
return expiresAtMs(fromFile) > expiresAtMs(fromKeychain) ? fromFile : fromKeychain;
|
|
200
|
+
}
|
|
201
|
+
/** The person session from the Keychain and the file, by the rules above. */
|
|
202
|
+
export function readPersonTokensWithKeychain(file = path.join(os.homedir(), ".hq", "cognito-tokens.json"), opts = {}) {
|
|
203
|
+
if (!fs.existsSync(file))
|
|
204
|
+
return { tokens: null, source: "none" };
|
|
205
|
+
let fromFile;
|
|
206
|
+
try {
|
|
207
|
+
fromFile = parseTokenJson(fs.readFileSync(file, "utf8"));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
fromFile = null;
|
|
211
|
+
}
|
|
212
|
+
const fromKeychain = readKeychainTokens(opts);
|
|
213
|
+
const chosen = pickNewerSession(fromKeychain, fromFile);
|
|
214
|
+
if (!chosen)
|
|
215
|
+
return { tokens: null, source: "none" };
|
|
216
|
+
return { tokens: chosen, source: chosen === fromKeychain ? "keychain" : "file" };
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=token-keychain.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.107",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
49
49
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
50
50
|
"@aws-sdk/dsql-signer": "^3.1146.0",
|
|
51
|
-
"@indigoai-us/hq-cloud": "~6.18.
|
|
51
|
+
"@indigoai-us/hq-cloud": "~6.18.66",
|
|
52
52
|
"@indigoai-us/hq-flags-client": "^0.1.2",
|
|
53
53
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
54
54
|
"@sentry/node": "^10.49.0",
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ontology-audience-grant.sh — grant each scoped audience folder to exactly the
|
|
3
|
-
# people in its audience, and prove every grant by readback.
|
|
4
|
-
# Spec: core/knowledge/public/hq-core/ontology-local-spec.md
|
|
5
|
-
#
|
|
6
|
-
# Usage: ontology-audience-grant.sh --company <co> [--dry-run]
|
|
7
|
-
#
|
|
8
|
-
# For every signals/@{key}/_audience.yaml under companies/<co>/:
|
|
9
|
-
# - grants read on signals/@{key}/, ontology/facts/@{key}/, and each
|
|
10
|
-
# sources/{channel}/@{key}/ that exists, to each principal that is an
|
|
11
|
-
# active company member (non-members are skipped with a warning);
|
|
12
|
-
# - one prefix grant per principal per folder, never per file;
|
|
13
|
-
# - each grant is the recursive shared glob `<folder>/*` (a bare `<folder>/`
|
|
14
|
-
# is a private create-only folder since hq-pro #3662);
|
|
15
|
-
# - reads every grant back with `hq files acl --json`; a missing grant exits 3.
|
|
16
|
-
# Company-audience paths get @all read on signals/{type}/ and
|
|
17
|
-
# ontology/facts/@company/ only. Nothing else under ontology/, signals/,
|
|
18
|
-
# sources/ is granted to @all.
|
|
19
|
-
# Local-only companies (company.yaml cloud: false or absent cloud_uid) exit 0
|
|
20
|
-
# with "local-only, no grants".
|
|
21
|
-
set -uo pipefail
|
|
22
|
-
HQ="${HQ_BIN:-hq}"
|
|
23
|
-
root="${HQ_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
24
|
-
co="" dry=0
|
|
25
|
-
while [ $# -gt 0 ]; do
|
|
26
|
-
case "$1" in --company) co="$2"; shift 2 ;; --dry-run) dry=1; shift ;; *) echo "unknown flag $1" >&2; exit 2 ;; esac
|
|
27
|
-
done
|
|
28
|
-
[ -n "$co" ] || { echo "usage: ontology-audience-grant.sh --company <co> [--dry-run]" >&2; exit 2; }
|
|
29
|
-
C="$root/companies/$co"; [ -d "$C" ] || { echo "no company $co" >&2; exit 2; }
|
|
30
|
-
|
|
31
|
-
cloud=no
|
|
32
|
-
grep -Eq '^cloud:[[:space:]]*true' "$C/company.yaml" 2>/dev/null && cloud=yes
|
|
33
|
-
if [ "$cloud" = no ] && [ -f "$root/companies/manifest.yaml" ]; then
|
|
34
|
-
# manifest: companies:\n <co>:\n cloud_uid: ... (two-space company keys)
|
|
35
|
-
awk -v co="$co" '/^ [^ #][^:]*:/{inco=($1==co":")} inco && /^ cloud_uid:[[:space:]]*[^[:space:]]/{f=1} END{exit !f}' "$root/companies/manifest.yaml" && cloud=yes
|
|
36
|
-
fi
|
|
37
|
-
[ "$cloud" = yes ] || { echo "local-only, no grants"; exit 0; }
|
|
38
|
-
|
|
39
|
-
members="$("$HQ" members --company "$co" list 2>/dev/null | awk 'NR>1{print tolower($1)}')"
|
|
40
|
-
[ -n "$members" ] || { echo "could not list members of $co" >&2; exit 3; }
|
|
41
|
-
|
|
42
|
-
granted=0 skipped=0 failed=0
|
|
43
|
-
grant() { # <folder/> <principal>
|
|
44
|
-
# Grant the recursive shared glob `<folder>/*`, never the bare `<folder>/`:
|
|
45
|
-
# since hq-pro #3662 a bare trailing slash is a private create-only folder
|
|
46
|
-
# (read on it exposes no children) and ACL reads echo the pattern verbatim.
|
|
47
|
-
local p="${1%/}/*" who="$2"
|
|
48
|
-
if [ "$dry" = 1 ]; then echo "would grant read $p -> $who"; return 0; fi
|
|
49
|
-
"$HQ" files --company "$co" share "$p" --with "$who" --permission read >/dev/null 2>&1 || true
|
|
50
|
-
if "$HQ" files --company "$co" acl "$p" --json 2>/dev/null | jq -e --arg w "$who" \
|
|
51
|
-
'.direct[]? | select((.granteeId==$w) or ($w=="@all" and .granteeType=="company-wide")) | select(.permission=="read" or .permission=="write" or .permission=="admin")' >/dev/null; then
|
|
52
|
-
granted=$((granted+1)); echo "granted read $p -> $who"
|
|
53
|
-
else
|
|
54
|
-
failed=$((failed+1)); echo "READBACK FAILED: $p -> $who" >&2
|
|
55
|
-
fi
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
for aud in "$C"/signals/@*/_audience.yaml; do
|
|
59
|
-
[ -f "$aud" ] || continue
|
|
60
|
-
key="$(basename "$(dirname "$aud")")"; key="${key#@}"
|
|
61
|
-
principals="$(sed -n 's/^principals:[[:space:]]*\[\(.*\)\]/\1/p' "$aud" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | awk '/@/ { print tolower($0); next } { print }' | grep -v '^$')"
|
|
62
|
-
prefixes="signals/@$key/"
|
|
63
|
-
[ -d "$C/ontology/facts/@$key" ] && prefixes="$prefixes ontology/facts/@$key/"
|
|
64
|
-
for d in "$C"/sources/*/@"$key"; do [ -d "$d" ] && prefixes="$prefixes sources/$(basename "$(dirname "$d")")/@$key/"; done
|
|
65
|
-
for who in $principals; do
|
|
66
|
-
# prs_/agt_ principals come from the source's own vault ACL (e.g. meeting
|
|
67
|
-
# attendees), so they are already company identities; emails must be members.
|
|
68
|
-
case "$who" in
|
|
69
|
-
prs_*|agt_*) ;;
|
|
70
|
-
*) if ! grep -qxF "$who" <<<"$members"; then echo "skip $who (not a member of $co)" >&2; skipped=$((skipped+1)); continue; fi ;;
|
|
71
|
-
esac
|
|
72
|
-
for p in $prefixes; do grant "$p" "$who"; done
|
|
73
|
-
done
|
|
74
|
-
done
|
|
75
|
-
[ -d "$C/ontology/facts/@company" ] && grant "ontology/facts/@company/" "@all"
|
|
76
|
-
for t in action_item commitment decision risk question key_point participant_contribution summary; do
|
|
77
|
-
[ -d "$C/signals/$t" ] && grant "signals/$t/" "@all"
|
|
78
|
-
done
|
|
79
|
-
|
|
80
|
-
echo "audience grants for $co: $granted granted, $skipped skipped, $failed failed"
|
|
81
|
-
[ "$failed" -eq 0 ] || exit 3
|