@indigoai-us/hq-cli 5.342.1 → 5.342.3

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.
@@ -20,7 +20,9 @@ async function refresh() {
20
20
  endpoint,
21
21
  ...(companyUid ? { companyUid } : {}),
22
22
  ...(companyIdentifiers.length ? { companyIdentifiers } : {}),
23
- getToken: () => loadCachedTokens()?.idToken ?? "",
23
+ getToken: () => typeof data.idToken === "string" && data.idToken.length > 0
24
+ ? data.idToken
25
+ : loadCachedTokens()?.idToken ?? "",
24
26
  refreshIntervalMs: 0,
25
27
  onError: () => { },
26
28
  });
@@ -28,9 +30,12 @@ async function refresh() {
28
30
  const snapshot = client.snapshot();
29
31
  if (snapshot)
30
32
  parentPort.postMessage(snapshot);
33
+ else
34
+ parentPort.postMessage({ status: "snapshot_unavailable" });
31
35
  }
32
36
  catch {
33
37
  // The parent is deliberately fail-open and has the legacy fallback.
38
+ parentPort?.postMessage({ status: "snapshot_unavailable" });
34
39
  }
35
40
  }
36
41
  void refresh();
@@ -15,11 +15,18 @@ export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
15
15
  export type FlagReader = Pick<FlagClient, "isEnabled">;
16
16
  interface WorkerLike {
17
17
  on(event: "message", listener: (value: unknown) => void): unknown;
18
- on(event: "error", listener: () => void): unknown;
18
+ on(event: "error", listener: (error: Error) => void): unknown;
19
+ ref?(): unknown;
19
20
  unref(): void;
20
21
  }
21
22
  export interface FlagRegistryDependencies {
22
23
  endpoint?: string;
24
+ /** An explicit `--company` scope, which takes precedence over ambient binding. */
25
+ companyScope?: string;
26
+ /** CLI argv used to resolve explicit company scope before the command parser runs. */
27
+ argv?: readonly string[];
28
+ /** Optional machine ID token already cached by the CLI, never logged or persisted. */
29
+ idToken?: string;
23
30
  createWorker?: (filename: string | URL, options: WorkerOptions) => WorkerLike;
24
31
  sentry?: FlagRegistrySentry;
25
32
  /** Test seam for exercising the no-source fail-open path. */
@@ -31,6 +38,10 @@ export interface CompanyFlagGateDependencies {
31
38
  requestTimeoutMs?: number;
32
39
  createClient?: typeof createFlagClient;
33
40
  }
41
+ /** Extract only a CLI-level --company option; values after `--` belong to child commands. */
42
+ export declare function explicitCompanyScopeFromArgv(argv: readonly string[]): string | undefined;
43
+ /** Wait briefly for the current company refresh to persist or report completion. */
44
+ export declare function waitForFlagRegistryRefresh(timeoutMs?: number, companyUid?: string): Promise<void>;
34
45
  /** Create the same snapshot-backed reader used by process gates. */
35
46
  export declare function createProcessSnapshotReader(companyIdentifiers: string[], getSnapshot?: () => FlagSnapshot | null, env?: NodeJS.ProcessEnv): FlagReader;
36
47
  /**
@@ -10,6 +10,7 @@ import { createFlagClient, isFlagSnapshot, readLocalOverride, resolveFlag, } fro
10
10
  import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
11
11
  import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
12
12
  import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
13
+ import { registerFlagRegistryRefreshWaiter } from "./flag-registry-refresh-wait.js";
13
14
  import { writeFlagSnapshotFile } from "./flag-snapshot-file.js";
14
15
  function captureFlagRegistryException(error, tags, sentry) {
15
16
  if (!sentry)
@@ -19,12 +20,74 @@ function captureFlagRegistryException(error, tags, sentry) {
19
20
  export { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
20
21
  export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
21
22
  const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
23
+ /** Extract only a CLI-level --company option; values after `--` belong to child commands. */
24
+ export function explicitCompanyScopeFromArgv(argv) {
25
+ for (let index = 2; index < argv.length; index += 1) {
26
+ const argument = argv[index];
27
+ if (argument === "--")
28
+ return undefined;
29
+ if (argument === "--company") {
30
+ const value = argv[index + 1]?.trim();
31
+ return value && !value.startsWith("-") ? value : undefined;
32
+ }
33
+ if (argument.startsWith("--company=")) {
34
+ const value = argument.slice("--company=".length).trim();
35
+ return value || undefined;
36
+ }
37
+ }
38
+ return undefined;
39
+ }
22
40
  function normalizedCompanyUid(value) {
23
41
  const candidate = value?.trim() ?? "";
24
42
  return COMPANY_UID_RE.test(candidate) ? candidate : undefined;
25
43
  }
26
44
  let heldSnapshot = null;
27
45
  let processReader = null;
46
+ let refreshCompletion = Promise.resolve();
47
+ let completeRefresh = null;
48
+ let refreshWorker = null;
49
+ let refreshCompanyUid;
50
+ let refreshPendingSnapshot = null;
51
+ let refreshSentry;
52
+ /** Wait briefly for the current company refresh to persist or report completion. */
53
+ export async function waitForFlagRegistryRefresh(timeoutMs = 500, companyUid) {
54
+ const resolvedCompanyUid = normalizedCompanyUid(companyUid);
55
+ if (resolvedCompanyUid)
56
+ refreshCompanyUid = resolvedCompanyUid;
57
+ if (refreshPendingSnapshot && refreshCompanyUid) {
58
+ try {
59
+ writeFlagSnapshotFile(refreshCompanyUid, refreshPendingSnapshot.flags);
60
+ }
61
+ catch (error) {
62
+ captureFlagRegistryException(error instanceof Error ? error : new Error(String(error)), {
63
+ component: "flag-snapshot-cache-write",
64
+ company_uid_present: "true",
65
+ }, refreshSentry);
66
+ }
67
+ refreshPendingSnapshot = null;
68
+ completeRefresh?.();
69
+ completeRefresh = null;
70
+ }
71
+ if (!completeRefresh)
72
+ return;
73
+ const worker = refreshWorker;
74
+ worker?.ref?.();
75
+ let timer;
76
+ try {
77
+ await Promise.race([
78
+ refreshCompletion,
79
+ new Promise((resolve) => {
80
+ timer = setTimeout(resolve, Math.max(0, timeoutMs));
81
+ }),
82
+ ]);
83
+ }
84
+ finally {
85
+ if (timer)
86
+ clearTimeout(timer);
87
+ worker?.unref();
88
+ }
89
+ }
90
+ registerFlagRegistryRefreshWaiter(waitForFlagRegistryRefresh);
28
91
  function processLocalOverride(flagKey, env = process.env) {
29
92
  return readLocalOverride(flagKey, env);
30
93
  }
@@ -61,10 +124,37 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
61
124
  if (processReader)
62
125
  return processReader;
63
126
  const endpoint = resolveFlagRegistryEndpoint(dependencies.endpoint, process.env);
64
- const company = resolveProcessFlagCompanyContext();
127
+ const companyScope = dependencies.companyScope ?? explicitCompanyScopeFromArgv(dependencies.argv ?? []);
128
+ const companyEnv = companyScope
129
+ ? {
130
+ ...process.env,
131
+ HQ_COMPANY_UID: COMPANY_UID_RE.test(companyScope)
132
+ ? companyScope
133
+ : "",
134
+ HQ_COMPANY_SLUG: COMPANY_UID_RE.test(companyScope)
135
+ ? ""
136
+ : companyScope,
137
+ }
138
+ : process.env;
139
+ const company = resolveProcessFlagCompanyContext(companyEnv);
65
140
  const sentry = dependencies.sentry;
66
141
  const companyUid = company.companyUid;
67
142
  const companyIdentifiers = [companyUid, company.companySlug].filter((value) => Boolean(value));
143
+ refreshCompletion = Promise.resolve();
144
+ completeRefresh = null;
145
+ refreshWorker = null;
146
+ refreshCompanyUid = companyUid;
147
+ refreshPendingSnapshot = null;
148
+ refreshSentry = sentry;
149
+ if (companyUid || company.companySlug) {
150
+ refreshCompletion = new Promise((resolve) => {
151
+ completeRefresh = resolve;
152
+ });
153
+ }
154
+ const finishRefresh = () => {
155
+ completeRefresh?.();
156
+ completeRefresh = null;
157
+ };
68
158
  processReader = processSnapshotReader(companyIdentifiers);
69
159
  try {
70
160
  const embeddedSource = dependencies.workerSource ?? FLAG_REGISTRY_WORKER_SOURCE;
@@ -76,6 +166,7 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
76
166
  worker: "flag-registry",
77
167
  flag_worker_failure: "embedded_source_unavailable",
78
168
  });
169
+ finishRefresh();
79
170
  return processReader;
80
171
  }
81
172
  // This source is embedded at build time, so it remains available while an
@@ -85,6 +176,9 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
85
176
  endpoint,
86
177
  ...(companyUid ? { companyUid } : {}),
87
178
  ...(company.companySlug ? { companyIdentifiers: [company.companySlug] } : {}),
179
+ ...((companyUid || company.companySlug) && dependencies.idToken
180
+ ? { idToken: dependencies.idToken }
181
+ : {}),
88
182
  },
89
183
  eval: true,
90
184
  };
@@ -100,12 +194,22 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
100
194
  return createWorker(embeddedSource, workerOptions);
101
195
  })
102
196
  : createWorker(embeddedSource, workerOptions);
197
+ refreshWorker = worker;
198
+ let released = false;
199
+ const releaseWorker = () => {
200
+ if (released)
201
+ return;
202
+ released = true;
203
+ worker.unref();
204
+ };
103
205
  worker.on("message", (value) => {
104
206
  if (isFlagSnapshot(value)) {
105
207
  heldSnapshot = value;
106
- if (companyUid) {
208
+ if (completeRefresh)
209
+ refreshPendingSnapshot = value;
210
+ if (refreshCompanyUid && refreshPendingSnapshot) {
107
211
  try {
108
- writeFlagSnapshotFile(companyUid, value.flags);
212
+ writeFlagSnapshotFile(refreshCompanyUid, value.flags);
109
213
  }
110
214
  catch (error) {
111
215
  captureFlagRegistryException(error instanceof Error ? error : new Error(String(error)), {
@@ -113,18 +217,28 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
113
217
  company_uid_present: "true",
114
218
  }, sentry);
115
219
  }
220
+ refreshPendingSnapshot = null;
221
+ finishRefresh();
116
222
  }
117
223
  }
224
+ else {
225
+ finishRefresh();
226
+ }
227
+ releaseWorker();
118
228
  });
119
229
  // A failed background refresh must stay invisible to an offline CLI.
120
- worker.on("error", () => { });
230
+ worker.on("error", () => {
231
+ finishRefresh();
232
+ releaseWorker();
233
+ });
121
234
  // Attach listeners first: Worker.on() re-refs the worker, so unref must be
122
235
  // last for the parent process to remain free to exit immediately.
123
- worker.unref();
236
+ releaseWorker();
124
237
  }
125
238
  catch {
126
239
  // Construction can fail in restricted Node runtimes. The reader is still
127
240
  // safe: it has no snapshot, so it resolves through the legacy vocabulary.
241
+ finishRefresh();
128
242
  }
129
243
  return processReader;
130
244
  }
@@ -198,5 +312,11 @@ export function currentFlagSnapshot() {
198
312
  export function _resetFlagRegistryForTests() {
199
313
  heldSnapshot = null;
200
314
  processReader = null;
315
+ refreshCompletion = Promise.resolve();
316
+ completeRefresh = null;
317
+ refreshWorker = null;
318
+ refreshCompanyUid = undefined;
319
+ refreshPendingSnapshot = null;
320
+ refreshSentry = undefined;
201
321
  }
202
322
  //# sourceMappingURL=flag-registry.js.map
package/dist/main.js CHANGED
@@ -37,7 +37,7 @@ import { isInterceptedProcessExit } from "./utils/intercepted-process-exit.js";
37
37
  import { isAuthError } from "./utils/auth-error.js";
38
38
  import { browserLoginAbandonedMessage } from "./utils/browser-login-abandoned.js";
39
39
  import { callbackPortBusyMessage, DEFAULT_CALLBACK_PORT } from "./utils/callback-port-busy.js";
40
- import { DEFAULT_COGNITO } from "./utils/cognito-session.js";
40
+ import { DEFAULT_COGNITO, isMachineIdentity, loadMachineCachedTokens, } from "./utils/cognito-session.js";
41
41
  import { isCompanySelectionError } from "./utils/company-selection-error.js";
42
42
  import { canOfferTeamUpgrade, formatPlanGateError, offerTeamUpgrade, offerUpgradeLink, planGateErrorFromVaultError, } from "./utils/plan-gate-error.js";
43
43
  import { upgradeToTeam } from "./utils/team-upgrade.js";
@@ -170,7 +170,13 @@ export async function runCli() {
170
170
  const isAuthLogin = process.argv[2] === "auth" && process.argv[3] === "login";
171
171
  if (!isAuthLogin) {
172
172
  runGlobalNetworkLifecycle(process.argv, () => {
173
- void kickFlagRegistryReadiness({ sentry: Sentry });
173
+ void kickFlagRegistryReadiness({
174
+ sentry: Sentry,
175
+ argv: process.argv,
176
+ ...(isMachineIdentity()
177
+ ? { idToken: loadMachineCachedTokens()?.idToken }
178
+ : {}),
179
+ });
174
180
  });
175
181
  }
176
182
  // Set when a self-update re-exec'd this command on a newer CLI: the child
@@ -0,0 +1,103 @@
1
+ import { writeSync } from "node:fs";
2
+ /**
3
+ * A browser login is safe only when a person can answer it. This mirrors the
4
+ * repository's existing terminal convention in `canOfferTeamUpgrade`: both
5
+ * stdin and stderr must be TTYs, and CI is always non-interactive.
6
+ */
7
+ export declare function secretsCredentialOptions(): {
8
+ interactive: boolean;
9
+ };
10
+ export interface SecretsScopeOpts {
11
+ company?: string;
12
+ personal?: boolean;
13
+ }
14
+ export declare function scopeOpts(opts: SecretsScopeOpts): {
15
+ personal: boolean;
16
+ companySlug: string | undefined;
17
+ };
18
+ export type SecretTier = "standard" | "sensitive" | "nuclear";
19
+ export type SecretScriptLockMode = "off" | "enforced";
20
+ export type SecretUsageChannel = "run" | "exec" | "env" | "sandbox" | "reveal" | "submit-link";
21
+ export interface SecretScriptUsage {
22
+ scriptId: string;
23
+ path: string;
24
+ sha256: string;
25
+ attestationLevel: string;
26
+ }
27
+ export interface SecretUsage {
28
+ channel: SecretUsageChannel;
29
+ reason?: string;
30
+ script?: SecretScriptUsage;
31
+ }
32
+ export interface SecretMetadata {
33
+ tier?: SecretTier;
34
+ scriptLock?: {
35
+ mode?: SecretScriptLockMode;
36
+ requiredAttestation?: string;
37
+ };
38
+ cacheTtlMs?: number;
39
+ revokedAt?: string | null;
40
+ }
41
+ export interface SecretLoadSuccessRow extends SecretMetadata {
42
+ name: string;
43
+ value?: string;
44
+ version?: number;
45
+ }
46
+ export interface SecretLoadResponse {
47
+ secrets: SecretLoadSuccessRow[];
48
+ errors: Array<{
49
+ name: string;
50
+ code: string;
51
+ message?: string;
52
+ }>;
53
+ }
54
+ export declare function highSecuritySandboxOnlyMessage(secretName?: string): string;
55
+ export declare function normalizeSecretTier(tier?: string): SecretTier;
56
+ export declare function normalizeScriptLockMode(mode?: string): SecretScriptLockMode;
57
+ export declare function extractApiMessage(body: Record<string, unknown>, fallback: string): string;
58
+ export declare function buildSecretUsage(channel: SecretUsageChannel, scriptPath?: string, scriptId?: string, attestationLevel?: string): Promise<SecretUsage>;
59
+ export declare function parseSecretNameList(input: string[] | undefined): string[];
60
+ export declare function loadRevealedSecrets(token: string, companyUid: string, keys: string[], usage?: SecretUsage, options?: {
61
+ allowOfflineFallback?: boolean;
62
+ }): Promise<Map<string, string>>;
63
+ /**
64
+ * Emit `hq secrets env` output and terminate deterministically.
65
+ *
66
+ * The process MUST exit even when the consuming pipe has gone away. The
67
+ * orphaned-process leak (personal/projects/hq-secrets-process-leak) was caused
68
+ * by relying on `process.stdout.write(payload, () => process.exit(0))`: when the
69
+ * parent shell reading `source <(hq secrets env …)` is killed on timeout, the
70
+ * stdout pipe loses its reader, the write callback never fires, `process.exit(0)`
71
+ * is never reached, and — because `env` forces no other exit — the process parks
72
+ * in `epoll_pwait(-1)` forever, reparented to PID 1. Hundreds accumulated this
73
+ * way, exhausting swap and driving load into triple digits.
74
+ *
75
+ * Writing synchronously to fd 1 and then exiting unconditionally removes every
76
+ * dependency on the event loop draining and on the reader still being attached:
77
+ * - a live reader receives the export lines (synchronous write reaches the
78
+ * kernel pipe buffer before we exit, so nothing is truncated);
79
+ * - a dead reader surfaces as EPIPE, which we swallow — the secret is already
80
+ * resolved and there is nothing left to deliver, so a clean exit is correct;
81
+ * - EAGAIN (stdout in non-blocking mode with a momentarily full buffer) is
82
+ * retried a bounded number of times. `env` payloads are a few short export
83
+ * lines, far under the 64 KiB pipe buffer, so this effectively never spins.
84
+ *
85
+ * `exit` and `write` are injectable for tests; production uses the real ones.
86
+ */
87
+ export declare function emitEnvExportsAndExit(payload: string, exit?: (code: number) => never, write?: typeof writeSync): never;
88
+ /**
89
+ * Shared `secrets env` action for Commander and the default-off lightweight
90
+ * entrypoint. The fast entrypoint still runs the same credential, usage-audit,
91
+ * server authorization, cache-fallback, redaction, and output code.
92
+ */
93
+ export declare function runSecretsEnvCommand(opts: {
94
+ only: string[];
95
+ script?: string;
96
+ scriptId?: string;
97
+ }, parentScope: {
98
+ company?: string;
99
+ personal?: boolean;
100
+ }, options?: {
101
+ fallbackOnAuthFailure?: boolean;
102
+ }): Promise<"handled" | "fallback">;
103
+ //# sourceMappingURL=secrets-env-action.d.ts.map