@indigoai-us/hq-cli 5.18.1 → 5.18.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.
@@ -20,13 +20,29 @@ export interface InviteOptions {
20
20
  callerUid: string;
21
21
  token: string;
22
22
  }
23
+ /**
24
+ * Outcome of `hq members invite`. Two shapes depending on server schema:
25
+ *
26
+ * - **schemaVersion ≤ 1** — server returns a random `inviteToken` the
27
+ * invitee redeems via the `hq://accept/{token}` magic link. `magicLink`
28
+ * is populated so the caller can print or paste it.
29
+ * - **schemaVersion 2+** (current production) — membership row is
30
+ * email-keyed and authoritative. There is no token; the invitee
31
+ * accepts by signing into HQ with the same email. `inviteToken` +
32
+ * `magicLink` are both `undefined`; the caller prints sign-in
33
+ * instructions instead.
34
+ *
35
+ * `membership` is always populated when the server returned 2xx.
36
+ */
23
37
  export interface InviteResult {
24
- inviteToken: string;
25
- magicLink: string;
38
+ inviteToken?: string;
39
+ magicLink?: string;
26
40
  membership: {
41
+ membershipKey?: string;
27
42
  role: string;
28
43
  status: string;
29
44
  inviteToken?: string;
45
+ inviteeEmail?: string;
30
46
  };
31
47
  }
32
48
  export interface DetectedTarget {
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5f8b5b62-a00e-5ca3-b293-23906579a464")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="20c76490-0485-592f-856b-3b20e403a392")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import { ensureCognitoToken } from "../utils/cognito-session.js";
5
5
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
@@ -72,19 +72,26 @@ export async function inviteMember(options) {
72
72
  throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
73
73
  }
74
74
  const data = (await res.json());
75
- // The token may arrive at the response root OR nested on the membership row,
76
- // depending on vault-service version. Resolve from either; never emit
77
- // `hq://accept/undefined` (a broken link that looks like success).
78
- const inviteToken = data.inviteToken ?? data.membership?.inviteToken;
79
- if (!inviteToken) {
75
+ if (!data.membership) {
80
76
  const keys = Object.keys(data ?? {}).join(", ") || "<empty>";
81
- throw new Error(`Invite was created but the server response did not include an invite token (response keys: ${keys}). ` +
82
- "Run `hq members list` to retrieve the pending invite, or upgrade hq.");
77
+ throw new Error(`Invite endpoint returned 2xx with no membership row (response keys: ${keys}). ` +
78
+ "This is a server-side regression file an issue.");
83
79
  }
80
+ // Two server schemas in the wild:
81
+ // - Legacy (schemaVersion ≤ 1): response carries a random `inviteToken`
82
+ // the invitee redeems via `hq://accept/{token}`.
83
+ // - Current (schemaVersion 2+): membership row is email-keyed and
84
+ // authoritative — there is no token. The invitee accepts by signing
85
+ // into HQ with the invited email. The CLI must NOT throw here
86
+ // (previously did: "response did not include an invite token") — the
87
+ // invite IS successfully created on the server; the caller just gets
88
+ // undefined for inviteToken/magicLink and prints sign-in instructions.
89
+ const inviteToken = data.inviteToken ?? data.membership.inviteToken;
84
90
  return {
85
- inviteToken,
86
- magicLink: `hq://accept/${inviteToken}`,
87
- membership: data.membership ?? { role: options.role, status: "pending" },
91
+ ...(inviteToken
92
+ ? { inviteToken, magicLink: `hq://accept/${inviteToken}` }
93
+ : {}),
94
+ membership: data.membership,
88
95
  };
89
96
  }
90
97
  export class InviteHttpError extends Error {
@@ -122,8 +129,11 @@ export async function listPendingInvites(token, companyUid) {
122
129
  const err = (await res.json().catch(() => ({})));
123
130
  throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
124
131
  }
132
+ // Server schema: `{ pending: [...] }`. Earlier dev branches used
133
+ // `{ invites: [...] }` which the CLI still accepts as a fallback for
134
+ // operators running staging stages that haven't caught up yet.
125
135
  const data = (await res.json());
126
- return data?.invites ?? [];
136
+ return data?.pending ?? data?.invites ?? [];
127
137
  }
128
138
  export async function revokeInvite(token, tokenOrKey, companyUid) {
129
139
  const res = await vaultApiFetch({
@@ -163,10 +173,28 @@ export function registerMembersCommand(program) {
163
173
  });
164
174
  console.log(chalk.green(`Invited ${target} as ${result.membership.role} (status: ${result.membership.status})`));
165
175
  console.log();
166
- console.log(chalk.bold("Magic link:"));
167
- console.log(` ${result.magicLink}`);
168
- console.log();
169
- console.log(chalk.dim("Share this link with the invitee. They can run `hq onboard join --invite-token <token>` to accept."));
176
+ if (result.magicLink) {
177
+ // Legacy server schema — magic-link redemption.
178
+ console.log(chalk.bold("Magic link:"));
179
+ console.log(` ${result.magicLink}`);
180
+ console.log();
181
+ console.log(chalk.dim("Share this link with the invitee. They can run `hq onboard join --invite-token <token>` to accept."));
182
+ }
183
+ else {
184
+ // schemaVersion 2+ — email-keyed authoritative membership row.
185
+ // No magic link to share; invitee accepts by signing into HQ.
186
+ const inviteeEmail = result.membership.inviteeEmail ??
187
+ (typeof target === "string" && target.includes("@")
188
+ ? target
189
+ : undefined);
190
+ console.log(chalk.bold("Next step:"));
191
+ console.log(` Tell ${inviteeEmail ?? "the invitee"} to sign into HQ at https://hq.getindigo.ai with that email.`);
192
+ console.log(chalk.dim(" The pending membership row claims itself on first sign-in — no separate token redemption."));
193
+ if (result.membership.membershipKey) {
194
+ console.log();
195
+ console.log(chalk.dim(` Membership key: ${result.membership.membershipKey}`));
196
+ }
197
+ }
170
198
  }
171
199
  catch (err) {
172
200
  if (err instanceof InviteHttpError) {
@@ -253,4 +281,4 @@ export function registerMembersCommand(program) {
253
281
  });
254
282
  }
255
283
  //# sourceMappingURL=members.js.map
256
- //# debugId=5f8b5b62-a00e-5ca3-b293-23906579a464
284
+ //# debugId=20c76490-0485-592f-856b-3b20e403a392
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.18.1",
3
+ "version": "5.18.2",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -246,13 +246,49 @@ describe("inviteMember", () => {
246
246
  expect(result.magicLink).toBe("hq://accept/tok_nested");
247
247
  });
248
248
 
249
- it("throws instead of emitting hq://accept/undefined when no token is present", async () => {
249
+ it("schemaVersion 2: no inviteToken in response success with undefined magicLink", async () => {
250
+ // 2026-05-21 fix: the server moved to email-keyed authoritative
251
+ // membership rows (schemaVersion 2) — the invite IS created, but
252
+ // there is no token to redeem. The CLI must NOT throw here; instead
253
+ // the action handler prints "sign in with the invited email"
254
+ // instructions. Previously this case threw "did not include an
255
+ // invite token" which produced false-failure UX on a working invite.
250
256
  fetchSpy.mockResolvedValueOnce(
251
- jsonResponse(200, {
252
- membership: { role: "admin", status: "pending" },
257
+ jsonResponse(201, {
258
+ membership: {
259
+ membershipKey: "email:alice@example.com#cmp_acme",
260
+ role: "member",
261
+ status: "pending",
262
+ inviteeEmail: "alice@example.com",
263
+ schemaVersion: 2,
264
+ },
253
265
  }),
254
266
  );
255
267
 
268
+ const result = await inviteMember({
269
+ target: "alice@example.com",
270
+ role: "member",
271
+ companyUid: "cmp_acme",
272
+ callerUid: "prs_admin",
273
+ token: "test-token",
274
+ });
275
+
276
+ expect(result.inviteToken).toBeUndefined();
277
+ expect(result.magicLink).toBeUndefined();
278
+ expect(result.membership.role).toBe("member");
279
+ expect(result.membership.status).toBe("pending");
280
+ expect(result.membership.membershipKey).toBe(
281
+ "email:alice@example.com#cmp_acme",
282
+ );
283
+ expect(result.membership.inviteeEmail).toBe("alice@example.com");
284
+ });
285
+
286
+ it("throws when the response has no membership row at all (server bug)", async () => {
287
+ // Belt-and-suspenders: a 2xx response with NO membership row is a
288
+ // server-side regression — surface it loudly so it doesn't silently
289
+ // succeed-but-do-nothing.
290
+ fetchSpy.mockResolvedValueOnce(jsonResponse(201, {}));
291
+
256
292
  await expect(
257
293
  inviteMember({
258
294
  target: "alice@example.com",
@@ -261,7 +297,7 @@ describe("inviteMember", () => {
261
297
  callerUid: "prs_admin",
262
298
  token: "test-token",
263
299
  }),
264
- ).rejects.toThrow(/did not include an invite token/);
300
+ ).rejects.toThrow(/no membership row/);
265
301
  });
266
302
  });
267
303
 
@@ -313,6 +349,47 @@ describe("listPendingInvites", () => {
313
349
  fetchSpy.mockResolvedValueOnce(jsonResponse(200, {}));
314
350
  await expect(listPendingInvites("test-token", "cmp_acme")).resolves.toEqual([]);
315
351
  });
352
+
353
+ it("schemaVersion 2: reads `pending` key (the canonical server response)", async () => {
354
+ // 2026-05-21 fix: live server returns `{ pending: [...] }`, not
355
+ // `{ invites: [...] }`. The CLI must read `pending` as the primary
356
+ // and fall back to `invites` for older stages.
357
+ fetchSpy.mockResolvedValueOnce(
358
+ jsonResponse(200, {
359
+ pending: [
360
+ {
361
+ membershipKey: "email:alice@example.com#cmp_acme",
362
+ inviteeEmail: "alice@example.com",
363
+ companyUid: "cmp_acme",
364
+ role: "member",
365
+ status: "pending",
366
+ invitedBy: "prs_admin",
367
+ invitedAt: "2026-05-21T12:00:00Z",
368
+ schemaVersion: 2,
369
+ },
370
+ ],
371
+ }),
372
+ );
373
+
374
+ const invites = await listPendingInvites("test-token", "cmp_acme");
375
+ expect(invites).toHaveLength(1);
376
+ expect(invites[0].membershipKey).toBe("email:alice@example.com#cmp_acme");
377
+ expect(invites[0].inviteeEmail).toBe("alice@example.com");
378
+ });
379
+
380
+ it("prefers `pending` over legacy `invites` when both present (server transition)", async () => {
381
+ // Defensive: an in-flight server deploy could briefly return BOTH
382
+ // fields. CLI takes the canonical `pending` key.
383
+ fetchSpy.mockResolvedValueOnce(
384
+ jsonResponse(200, {
385
+ pending: [{ membershipKey: "k_pending" } as never],
386
+ invites: [{ membershipKey: "k_legacy" } as never],
387
+ }),
388
+ );
389
+ const invites = await listPendingInvites("test-token", "cmp_acme");
390
+ expect(invites).toHaveLength(1);
391
+ expect(invites[0].membershipKey).toBe("k_pending");
392
+ });
316
393
  });
317
394
 
318
395
  // ---------------------------------------------------------------------------
@@ -38,10 +38,30 @@ export interface InviteOptions {
38
38
  token: string;
39
39
  }
40
40
 
41
+ /**
42
+ * Outcome of `hq members invite`. Two shapes depending on server schema:
43
+ *
44
+ * - **schemaVersion ≤ 1** — server returns a random `inviteToken` the
45
+ * invitee redeems via the `hq://accept/{token}` magic link. `magicLink`
46
+ * is populated so the caller can print or paste it.
47
+ * - **schemaVersion 2+** (current production) — membership row is
48
+ * email-keyed and authoritative. There is no token; the invitee
49
+ * accepts by signing into HQ with the same email. `inviteToken` +
50
+ * `magicLink` are both `undefined`; the caller prints sign-in
51
+ * instructions instead.
52
+ *
53
+ * `membership` is always populated when the server returned 2xx.
54
+ */
41
55
  export interface InviteResult {
42
- inviteToken: string;
43
- magicLink: string;
44
- membership: { role: string; status: string; inviteToken?: string };
56
+ inviteToken?: string;
57
+ magicLink?: string;
58
+ membership: {
59
+ membershipKey?: string;
60
+ role: string;
61
+ status: string;
62
+ inviteToken?: string;
63
+ inviteeEmail?: string;
64
+ };
45
65
  }
46
66
 
47
67
  export interface DetectedTarget {
@@ -137,24 +157,38 @@ export async function inviteMember(
137
157
  }
138
158
 
139
159
  const data = (await res.json()) as {
140
- membership?: { role: string; status: string; inviteToken?: string };
160
+ membership?: {
161
+ membershipKey?: string;
162
+ role: string;
163
+ status: string;
164
+ inviteToken?: string;
165
+ inviteeEmail?: string;
166
+ schemaVersion?: number;
167
+ };
141
168
  inviteToken?: string;
142
169
  };
143
- // The token may arrive at the response root OR nested on the membership row,
144
- // depending on vault-service version. Resolve from either; never emit
145
- // `hq://accept/undefined` (a broken link that looks like success).
146
- const inviteToken = data.inviteToken ?? data.membership?.inviteToken;
147
- if (!inviteToken) {
170
+ if (!data.membership) {
148
171
  const keys = Object.keys(data ?? {}).join(", ") || "<empty>";
149
172
  throw new Error(
150
- `Invite was created but the server response did not include an invite token (response keys: ${keys}). ` +
151
- "Run `hq members list` to retrieve the pending invite, or upgrade hq.",
173
+ `Invite endpoint returned 2xx with no membership row (response keys: ${keys}). ` +
174
+ "This is a server-side regression file an issue.",
152
175
  );
153
176
  }
177
+ // Two server schemas in the wild:
178
+ // - Legacy (schemaVersion ≤ 1): response carries a random `inviteToken`
179
+ // the invitee redeems via `hq://accept/{token}`.
180
+ // - Current (schemaVersion 2+): membership row is email-keyed and
181
+ // authoritative — there is no token. The invitee accepts by signing
182
+ // into HQ with the invited email. The CLI must NOT throw here
183
+ // (previously did: "response did not include an invite token") — the
184
+ // invite IS successfully created on the server; the caller just gets
185
+ // undefined for inviteToken/magicLink and prints sign-in instructions.
186
+ const inviteToken = data.inviteToken ?? data.membership.inviteToken;
154
187
  return {
155
- inviteToken,
156
- magicLink: `hq://accept/${inviteToken}`,
157
- membership: data.membership ?? { role: options.role, status: "pending" },
188
+ ...(inviteToken
189
+ ? { inviteToken, magicLink: `hq://accept/${inviteToken}` }
190
+ : {}),
191
+ membership: data.membership,
158
192
  };
159
193
  }
160
194
 
@@ -204,8 +238,14 @@ export async function listPendingInvites(
204
238
  err.code,
205
239
  );
206
240
  }
207
- const data = (await res.json()) as { invites?: PendingInvite[] | null };
208
- return data?.invites ?? [];
241
+ // Server schema: `{ pending: [...] }`. Earlier dev branches used
242
+ // `{ invites: [...] }` which the CLI still accepts as a fallback for
243
+ // operators running staging stages that haven't caught up yet.
244
+ const data = (await res.json()) as {
245
+ pending?: PendingInvite[] | null;
246
+ invites?: PendingInvite[] | null;
247
+ };
248
+ return data?.pending ?? data?.invites ?? [];
209
249
  }
210
250
 
211
251
  export async function revokeInvite(
@@ -275,14 +315,40 @@ export function registerMembersCommand(program: Command): void {
275
315
  ),
276
316
  );
277
317
  console.log();
278
- console.log(chalk.bold("Magic link:"));
279
- console.log(` ${result.magicLink}`);
280
- console.log();
281
- console.log(
282
- chalk.dim(
283
- "Share this link with the invitee. They can run `hq onboard join --invite-token <token>` to accept.",
284
- ),
285
- );
318
+ if (result.magicLink) {
319
+ // Legacy server schema — magic-link redemption.
320
+ console.log(chalk.bold("Magic link:"));
321
+ console.log(` ${result.magicLink}`);
322
+ console.log();
323
+ console.log(
324
+ chalk.dim(
325
+ "Share this link with the invitee. They can run `hq onboard join --invite-token <token>` to accept.",
326
+ ),
327
+ );
328
+ } else {
329
+ // schemaVersion 2+ — email-keyed authoritative membership row.
330
+ // No magic link to share; invitee accepts by signing into HQ.
331
+ const inviteeEmail =
332
+ result.membership.inviteeEmail ??
333
+ (typeof target === "string" && target.includes("@")
334
+ ? target
335
+ : undefined);
336
+ console.log(chalk.bold("Next step:"));
337
+ console.log(
338
+ ` Tell ${inviteeEmail ?? "the invitee"} to sign into HQ at https://hq.getindigo.ai with that email.`,
339
+ );
340
+ console.log(
341
+ chalk.dim(
342
+ " The pending membership row claims itself on first sign-in — no separate token redemption.",
343
+ ),
344
+ );
345
+ if (result.membership.membershipKey) {
346
+ console.log();
347
+ console.log(
348
+ chalk.dim(` Membership key: ${result.membership.membershipKey}`),
349
+ );
350
+ }
351
+ }
286
352
  } catch (err) {
287
353
  if (err instanceof InviteHttpError) {
288
354
  console.error(