@indigoai-us/hq-cli 5.77.12 → 5.77.13

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.
@@ -61,4 +61,49 @@ describe("packaging: runtime dependencies", () => {
61
61
  expect(pkg.dependencies).toHaveProperty("@aws-sdk/client-s3");
62
62
  expect(pkg.devDependencies).not.toHaveProperty("@aws-sdk/client-s3");
63
63
  });
64
+
65
+ it("ships the IoT data-plane client imported by the session heartbeat", () => {
66
+ expect(pkg.dependencies).toHaveProperty("@aws-sdk/client-iot-data-plane");
67
+ expect(pkg.devDependencies).not.toHaveProperty(
68
+ "@aws-sdk/client-iot-data-plane",
69
+ );
70
+ });
71
+ });
72
+
73
+ /**
74
+ * Regression pin for the 2026-07 dead-heartbeat incident.
75
+ *
76
+ * The Outpost's systemd unit invoked `outpost-session-heartbeat-runner`, a
77
+ * command that was never written. The provisioning test asserted only that the
78
+ * generated user-data CONTAINED that string, so it passed for the entire life
79
+ * of the bug while the box logged "command not found" every five seconds and
80
+ * the box audit reported the service healthy.
81
+ *
82
+ * The lesson: asserting that a command NAME appears somewhere proves nothing.
83
+ * Assert the command RESOLVES. If `hq outposts heartbeat` is ever renamed,
84
+ * un-registered, or dropped from the build, this fails here — before a box is
85
+ * provisioned against it.
86
+ */
87
+ describe("packaging: on-box commands the Outpost user-data invokes", () => {
88
+ it("registers `hq outposts heartbeat`", async () => {
89
+ const { Command } = await import("commander");
90
+ const { registerOutpostsCommand } = await import("./commands/outposts.js");
91
+
92
+ const program = new Command();
93
+ registerOutpostsCommand(program);
94
+
95
+ const outposts = program.commands.find((c) => c.name() === "outposts");
96
+ expect(outposts, "`hq outposts` must be registered").toBeDefined();
97
+
98
+ const heartbeat = outposts?.commands.find((c) => c.name() === "heartbeat");
99
+ expect(heartbeat, "`hq outposts heartbeat` must be registered").toBeDefined();
100
+
101
+ // The flags the systemd unit passes must all exist, or the unit dies on an
102
+ // "unknown option" at boot — a failure mode indistinguishable from the one
103
+ // this test exists to prevent.
104
+ const flags = (heartbeat?.options ?? []).map((o) => o.long);
105
+ expect(flags).toEqual(
106
+ expect.arrayContaining(["--once", "--interval", "--home", "--api-base-url"]),
107
+ );
108
+ });
64
109
  });
@@ -5,6 +5,13 @@ import { CompanySelectionError } from './company-selection-error.js';
5
5
  import { recordPlanLimitStatus } from '../lib/plan-limit-nag.js';
6
6
 
7
7
  export interface VaultApiOptions {
8
+ /**
9
+ * Control-plane origin. Defaults to DEFAULT_VAULT_API_URL. Set it when a
10
+ * caller has been pointed at another deployment, so identity and every
11
+ * other call resolve against the SAME plane — a token minted for one and
12
+ * sent to another is simply rejected, which reads as an auth failure.
13
+ */
14
+ baseUrl?: string;
8
15
  token: string;
9
16
  path: string;
10
17
  method?: string;
@@ -72,7 +79,7 @@ async function peekPlanLimitStatus(response: Response): Promise<Response> {
72
79
  }
73
80
 
74
81
  export async function vaultApiFetch(opts: VaultApiOptions): Promise<Response> {
75
- const url = new URL(opts.path, DEFAULT_VAULT_API_URL);
82
+ const url = new URL(opts.path, opts.baseUrl ?? DEFAULT_VAULT_API_URL);
76
83
  if (opts.query) {
77
84
  for (const [k, v] of Object.entries(opts.query)) {
78
85
  url.searchParams.set(k, v);
@@ -335,10 +342,14 @@ interface PersonEntity {
335
342
 
336
343
  // Same selection rule as the backend's `resolveCallerPersonUid`: ascending by
337
344
  // createdAt, tie-break by uid ascending. Returns the `prs_*` UID.
338
- export async function resolveCallerPersonUid(token: string): Promise<string> {
345
+ export async function resolveCallerPersonUid(
346
+ token: string,
347
+ baseUrl?: string,
348
+ ): Promise<string> {
339
349
  const res = await vaultApiFetch({
340
350
  token,
341
351
  path: '/entity/by-type/person',
352
+ baseUrl,
342
353
  });
343
354
  if (!res.ok) {
344
355
  throw new Error("Failed to fetch person entity — run `hq login` and try again");