@indigoai-us/hq-cli 5.18.2 → 5.18.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.
- package/CHANGELOG.md +43 -0
- package/dist/commands/members.d.ts +17 -0
- package/dist/commands/members.js +53 -11
- package/package.json +1 -1
- package/src/commands/members.test.ts +53 -0
- package/src/commands/members.ts +69 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.18.3] — 2026-05-21
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`hq members invite`: loud no-email warning + clearer next-steps.**
|
|
10
|
+
schemaVersion-2 invites only create the email-keyed pending DDB row —
|
|
11
|
+
the CLI does NOT send email. New output leads with a `chalk.yellow
|
|
12
|
+
⚠ No email was sent` warning + a two-option "do ONE of: manually
|
|
13
|
+
notify / use hq-console UI" block. Previous wording read like passive
|
|
14
|
+
next-step copy; admins mistook it for "email sent." (#22)
|
|
15
|
+
- **`hq members revoke <bare-email>`: no more 404.** Accepts bare email
|
|
16
|
+
and bare `prs_*` personUids, wrapping them into the canonical
|
|
17
|
+
`email:<email>#<companyUid>` / `<personUid>#<companyUid>` membership
|
|
18
|
+
key shape before posting to the server. (#22)
|
|
19
|
+
|
|
20
|
+
## [5.18.2] — 2026-05-21
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`hq members invite` + `hq members list` under schemaVersion 2 server.**
|
|
25
|
+
Aligns CLI with hq-pro membership schema-v2 (live-smoke surfaced regression). (#20, #21)
|
|
26
|
+
|
|
27
|
+
## [5.18.1] — 2026-05-21
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **Wire narrow-hint banner into per-company pull (US-011).** Banner now renders
|
|
32
|
+
on the per-company `hq sync pull` path, not just full-sync. (#18, #19)
|
|
33
|
+
|
|
34
|
+
## [5.18.0] — 2026-05-21
|
|
35
|
+
|
|
36
|
+
### Added — sync-browse-vs-sync
|
|
37
|
+
|
|
38
|
+
- **`hq sync mode <shared|narrow>`** — set per-membership sync mode (US-006).
|
|
39
|
+
- **`hq sync narrow --dry-run | --apply`** — preview/execute ACL narrowing for
|
|
40
|
+
the active company (US-007).
|
|
41
|
+
- **`hq files browse <prefix>` + `hq files cat <path>`** — read-only vault
|
|
42
|
+
browse via purpose=browse STS scope, distinct from sync (US-008).
|
|
43
|
+
- **Narrow-hint banner** — surfaces on `hq sync` when local membership is
|
|
44
|
+
shared-mode but cloud signals narrowing is recommended (US-011).
|
|
45
|
+
- Bumps `@indigoai-us/hq-cloud` to 5.23.0 (VaultClient SDK + sync engine
|
|
46
|
+
narrowing + journal v2). (#16, #17)
|
|
47
|
+
|
|
5
48
|
## [5.14.1] — 2026-05-14
|
|
6
49
|
|
|
7
50
|
### Fixed
|
|
@@ -66,6 +66,23 @@ export declare class InviteHttpError extends Error {
|
|
|
66
66
|
}
|
|
67
67
|
export declare function formatInviteHttpError(status: number, fallback: string, code?: string): string;
|
|
68
68
|
export declare function listPendingInvites(token: string, companyUid: string): Promise<PendingInvite[]>;
|
|
69
|
+
/**
|
|
70
|
+
* Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
|
|
71
|
+
* the server requires. Accepts three input forms:
|
|
72
|
+
*
|
|
73
|
+
* 1. Full membership key — already has `#<companyUid>`; passed through.
|
|
74
|
+
* Examples: `email:alice@example.com#cmp_abc`, `prs_abc#cmp_abc`.
|
|
75
|
+
* 2. Bare email — wrap as `email:<email>#<companyUid>`.
|
|
76
|
+
* 3. Bare personUid (`prs_*`) — wrap as `<personUid>#<companyUid>`.
|
|
77
|
+
* 4. Anything else (e.g. legacy schemaVersion-1 inviteToken) — pass through
|
|
78
|
+
* so the server can decide. Server may 404 if the token doesn't resolve.
|
|
79
|
+
*
|
|
80
|
+
* Pure function — no I/O — so it's trivially unit-testable. The previous
|
|
81
|
+
* shape sent the user's raw arg straight through, which meant `hq members
|
|
82
|
+
* revoke alice@example.com` always 404'd ("Invite not found") even when
|
|
83
|
+
* that exact email was just shown by `hq members list`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveRevokeTargetToMembershipKey(arg: string, companyUid: string): string;
|
|
69
86
|
export declare function revokeInvite(token: string, tokenOrKey: string, companyUid: string): Promise<void>;
|
|
70
87
|
export declare function registerMembersCommand(program: Command): void;
|
|
71
88
|
//# sourceMappingURL=members.d.ts.map
|
package/dist/commands/members.js
CHANGED
|
@@ -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]="
|
|
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]="c06b812a-ee4a-5701-a64b-cee2fcfef523")}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";
|
|
@@ -135,6 +135,34 @@ export async function listPendingInvites(token, companyUid) {
|
|
|
135
135
|
const data = (await res.json());
|
|
136
136
|
return data?.pending ?? data?.invites ?? [];
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
|
|
140
|
+
* the server requires. Accepts three input forms:
|
|
141
|
+
*
|
|
142
|
+
* 1. Full membership key — already has `#<companyUid>`; passed through.
|
|
143
|
+
* Examples: `email:alice@example.com#cmp_abc`, `prs_abc#cmp_abc`.
|
|
144
|
+
* 2. Bare email — wrap as `email:<email>#<companyUid>`.
|
|
145
|
+
* 3. Bare personUid (`prs_*`) — wrap as `<personUid>#<companyUid>`.
|
|
146
|
+
* 4. Anything else (e.g. legacy schemaVersion-1 inviteToken) — pass through
|
|
147
|
+
* so the server can decide. Server may 404 if the token doesn't resolve.
|
|
148
|
+
*
|
|
149
|
+
* Pure function — no I/O — so it's trivially unit-testable. The previous
|
|
150
|
+
* shape sent the user's raw arg straight through, which meant `hq members
|
|
151
|
+
* revoke alice@example.com` always 404'd ("Invite not found") even when
|
|
152
|
+
* that exact email was just shown by `hq members list`.
|
|
153
|
+
*/
|
|
154
|
+
export function resolveRevokeTargetToMembershipKey(arg, companyUid) {
|
|
155
|
+
if (arg.includes("#"))
|
|
156
|
+
return arg;
|
|
157
|
+
const detected = detectTarget(arg);
|
|
158
|
+
if (detected?.type === "email") {
|
|
159
|
+
return `email:${detected.value}#${companyUid}`;
|
|
160
|
+
}
|
|
161
|
+
if (detected?.type === "person") {
|
|
162
|
+
return `${detected.value}#${companyUid}`;
|
|
163
|
+
}
|
|
164
|
+
return arg;
|
|
165
|
+
}
|
|
138
166
|
export async function revokeInvite(token, tokenOrKey, companyUid) {
|
|
139
167
|
const res = await vaultApiFetch({
|
|
140
168
|
token,
|
|
@@ -183,16 +211,29 @@ export function registerMembersCommand(program) {
|
|
|
183
211
|
else {
|
|
184
212
|
// schemaVersion 2+ — email-keyed authoritative membership row.
|
|
185
213
|
// No magic link to share; invitee accepts by signing into HQ.
|
|
214
|
+
//
|
|
215
|
+
// CRITICAL UX NOTE: this CLI command does NOT send any email.
|
|
216
|
+
// hq-pro only writes the DDB pending row; only the hq-console UI
|
|
217
|
+
// path triggers Resend. Operators who run `hq members invite`
|
|
218
|
+
// expecting an email to fly out get silently broken flows. The
|
|
219
|
+
// output below uses chalk.yellow + an explicit "no email sent"
|
|
220
|
+
// line so this never sneaks past again.
|
|
186
221
|
const inviteeEmail = result.membership.inviteeEmail ??
|
|
187
222
|
(typeof target === "string" && target.includes("@")
|
|
188
223
|
? target
|
|
189
224
|
: undefined);
|
|
190
|
-
console.log(chalk.
|
|
191
|
-
console.log(
|
|
192
|
-
console.log(chalk.
|
|
225
|
+
console.log(chalk.yellow("⚠ No email was sent. `hq members invite` only creates the pending membership row."));
|
|
226
|
+
console.log();
|
|
227
|
+
console.log(chalk.bold("To complete the invite, do ONE of:"));
|
|
228
|
+
console.log(` 1. Manually notify ${inviteeEmail ?? "the invitee"}: ask them to sign into HQ`);
|
|
229
|
+
console.log(` at https://hq.getindigo.ai with that email address.`);
|
|
230
|
+
console.log(` 2. Or use the hq-console UI at https://hq.getindigo.ai to issue the`);
|
|
231
|
+
console.log(` invite instead — the UI path triggers an automated email via Resend.`);
|
|
232
|
+
console.log();
|
|
233
|
+
console.log(chalk.dim("The pending membership row claims itself on the invitee's first sign-in."));
|
|
193
234
|
if (result.membership.membershipKey) {
|
|
194
235
|
console.log();
|
|
195
|
-
console.log(chalk.dim(`
|
|
236
|
+
console.log(chalk.dim(`Membership key: ${result.membership.membershipKey}`));
|
|
196
237
|
}
|
|
197
238
|
}
|
|
198
239
|
}
|
|
@@ -253,15 +294,16 @@ export function registerMembersCommand(program) {
|
|
|
253
294
|
}
|
|
254
295
|
});
|
|
255
296
|
members
|
|
256
|
-
.command("revoke <
|
|
257
|
-
.description("Revoke a pending invite
|
|
258
|
-
.action(async (
|
|
297
|
+
.command("revoke <target>")
|
|
298
|
+
.description("Revoke a pending invite. Accepts an email, personUid, full membershipKey, or legacy inviteToken.")
|
|
299
|
+
.action(async (target) => {
|
|
259
300
|
try {
|
|
260
301
|
const token = await ensureCognitoToken();
|
|
261
302
|
const companySlug = members.opts().company;
|
|
262
303
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
263
|
-
|
|
264
|
-
|
|
304
|
+
const membershipKey = resolveRevokeTargetToMembershipKey(target, companyUid);
|
|
305
|
+
await revokeInvite(token, membershipKey, companyUid);
|
|
306
|
+
console.log(chalk.green(`Revoked invite '${membershipKey}'`));
|
|
265
307
|
}
|
|
266
308
|
catch (err) {
|
|
267
309
|
if (err instanceof InviteHttpError) {
|
|
@@ -281,4 +323,4 @@ export function registerMembersCommand(program) {
|
|
|
281
323
|
});
|
|
282
324
|
}
|
|
283
325
|
//# sourceMappingURL=members.js.map
|
|
284
|
-
//# debugId=
|
|
326
|
+
//# debugId=c06b812a-ee4a-5701-a64b-cee2fcfef523
|
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getCallerPersonUid,
|
|
19
19
|
inviteMember,
|
|
20
20
|
listPendingInvites,
|
|
21
|
+
resolveRevokeTargetToMembershipKey,
|
|
21
22
|
revokeInvite,
|
|
22
23
|
} from "./members.js";
|
|
23
24
|
|
|
@@ -414,6 +415,58 @@ describe("revokeInvite", () => {
|
|
|
414
415
|
});
|
|
415
416
|
});
|
|
416
417
|
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// resolveRevokeTargetToMembershipKey
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
describe("resolveRevokeTargetToMembershipKey", () => {
|
|
423
|
+
// Regression: `hq members revoke alice@example.com` used to send the raw
|
|
424
|
+
// email straight to /membership/revoke, which the server rejects with 404
|
|
425
|
+
// "Invite not found" because it keys on `email:<email>#<companyUid>`. Live
|
|
426
|
+
// smoke 2026-05-21 reproduced this against indigo.
|
|
427
|
+
it("wraps a bare email into the email-keyed membership shape", () => {
|
|
428
|
+
expect(
|
|
429
|
+
resolveRevokeTargetToMembershipKey("alice@example.com", "cmp_abc"),
|
|
430
|
+
).toBe("email:alice@example.com#cmp_abc");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("lowercases the email when wrapping", () => {
|
|
434
|
+
expect(
|
|
435
|
+
resolveRevokeTargetToMembershipKey("Alice@Example.COM", "cmp_abc"),
|
|
436
|
+
).toBe("email:alice@example.com#cmp_abc");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("wraps a bare personUid into the personUid-keyed membership shape", () => {
|
|
440
|
+
expect(resolveRevokeTargetToMembershipKey("prs_bob123", "cmp_abc")).toBe(
|
|
441
|
+
"prs_bob123#cmp_abc",
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("passes through a full email-keyed membership key unchanged", () => {
|
|
446
|
+
expect(
|
|
447
|
+
resolveRevokeTargetToMembershipKey(
|
|
448
|
+
"email:alice@example.com#cmp_abc",
|
|
449
|
+
"cmp_other",
|
|
450
|
+
),
|
|
451
|
+
).toBe("email:alice@example.com#cmp_abc");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("passes through a full personUid-keyed membership key unchanged", () => {
|
|
455
|
+
expect(
|
|
456
|
+
resolveRevokeTargetToMembershipKey("prs_bob#cmp_abc", "cmp_other"),
|
|
457
|
+
).toBe("prs_bob#cmp_abc");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("passes through unrecognized strings (legacy inviteToken, garbage)", () => {
|
|
461
|
+
// A schemaVersion-1 inviteToken is opaque base64url; we can't tell it
|
|
462
|
+
// apart from garbage. Send it to the server and let the server 404 if
|
|
463
|
+
// it doesn't resolve.
|
|
464
|
+
expect(
|
|
465
|
+
resolveRevokeTargetToMembershipKey("some-opaque-token", "cmp_abc"),
|
|
466
|
+
).toBe("some-opaque-token");
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
417
470
|
// ---------------------------------------------------------------------------
|
|
418
471
|
// formatInviteHttpError
|
|
419
472
|
// ---------------------------------------------------------------------------
|
package/src/commands/members.ts
CHANGED
|
@@ -248,6 +248,37 @@ export async function listPendingInvites(
|
|
|
248
248
|
return data?.pending ?? data?.invites ?? [];
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
|
|
253
|
+
* the server requires. Accepts three input forms:
|
|
254
|
+
*
|
|
255
|
+
* 1. Full membership key — already has `#<companyUid>`; passed through.
|
|
256
|
+
* Examples: `email:alice@example.com#cmp_abc`, `prs_abc#cmp_abc`.
|
|
257
|
+
* 2. Bare email — wrap as `email:<email>#<companyUid>`.
|
|
258
|
+
* 3. Bare personUid (`prs_*`) — wrap as `<personUid>#<companyUid>`.
|
|
259
|
+
* 4. Anything else (e.g. legacy schemaVersion-1 inviteToken) — pass through
|
|
260
|
+
* so the server can decide. Server may 404 if the token doesn't resolve.
|
|
261
|
+
*
|
|
262
|
+
* Pure function — no I/O — so it's trivially unit-testable. The previous
|
|
263
|
+
* shape sent the user's raw arg straight through, which meant `hq members
|
|
264
|
+
* revoke alice@example.com` always 404'd ("Invite not found") even when
|
|
265
|
+
* that exact email was just shown by `hq members list`.
|
|
266
|
+
*/
|
|
267
|
+
export function resolveRevokeTargetToMembershipKey(
|
|
268
|
+
arg: string,
|
|
269
|
+
companyUid: string,
|
|
270
|
+
): string {
|
|
271
|
+
if (arg.includes("#")) return arg;
|
|
272
|
+
const detected = detectTarget(arg);
|
|
273
|
+
if (detected?.type === "email") {
|
|
274
|
+
return `email:${detected.value}#${companyUid}`;
|
|
275
|
+
}
|
|
276
|
+
if (detected?.type === "person") {
|
|
277
|
+
return `${detected.value}#${companyUid}`;
|
|
278
|
+
}
|
|
279
|
+
return arg;
|
|
280
|
+
}
|
|
281
|
+
|
|
251
282
|
export async function revokeInvite(
|
|
252
283
|
token: string,
|
|
253
284
|
tokenOrKey: string,
|
|
@@ -328,24 +359,47 @@ export function registerMembersCommand(program: Command): void {
|
|
|
328
359
|
} else {
|
|
329
360
|
// schemaVersion 2+ — email-keyed authoritative membership row.
|
|
330
361
|
// No magic link to share; invitee accepts by signing into HQ.
|
|
362
|
+
//
|
|
363
|
+
// CRITICAL UX NOTE: this CLI command does NOT send any email.
|
|
364
|
+
// hq-pro only writes the DDB pending row; only the hq-console UI
|
|
365
|
+
// path triggers Resend. Operators who run `hq members invite`
|
|
366
|
+
// expecting an email to fly out get silently broken flows. The
|
|
367
|
+
// output below uses chalk.yellow + an explicit "no email sent"
|
|
368
|
+
// line so this never sneaks past again.
|
|
331
369
|
const inviteeEmail =
|
|
332
370
|
result.membership.inviteeEmail ??
|
|
333
371
|
(typeof target === "string" && target.includes("@")
|
|
334
372
|
? target
|
|
335
373
|
: undefined);
|
|
336
|
-
console.log(chalk.bold("Next step:"));
|
|
337
374
|
console.log(
|
|
338
|
-
|
|
375
|
+
chalk.yellow(
|
|
376
|
+
"⚠ No email was sent. `hq members invite` only creates the pending membership row.",
|
|
377
|
+
),
|
|
378
|
+
);
|
|
379
|
+
console.log();
|
|
380
|
+
console.log(chalk.bold("To complete the invite, do ONE of:"));
|
|
381
|
+
console.log(
|
|
382
|
+
` 1. Manually notify ${inviteeEmail ?? "the invitee"}: ask them to sign into HQ`,
|
|
383
|
+
);
|
|
384
|
+
console.log(
|
|
385
|
+
` at https://hq.getindigo.ai with that email address.`,
|
|
386
|
+
);
|
|
387
|
+
console.log(
|
|
388
|
+
` 2. Or use the hq-console UI at https://hq.getindigo.ai to issue the`,
|
|
339
389
|
);
|
|
390
|
+
console.log(
|
|
391
|
+
` invite instead — the UI path triggers an automated email via Resend.`,
|
|
392
|
+
);
|
|
393
|
+
console.log();
|
|
340
394
|
console.log(
|
|
341
395
|
chalk.dim(
|
|
342
|
-
"
|
|
396
|
+
"The pending membership row claims itself on the invitee's first sign-in.",
|
|
343
397
|
),
|
|
344
398
|
);
|
|
345
399
|
if (result.membership.membershipKey) {
|
|
346
400
|
console.log();
|
|
347
401
|
console.log(
|
|
348
|
-
chalk.dim(`
|
|
402
|
+
chalk.dim(`Membership key: ${result.membership.membershipKey}`),
|
|
349
403
|
);
|
|
350
404
|
}
|
|
351
405
|
}
|
|
@@ -431,16 +485,22 @@ export function registerMembersCommand(program: Command): void {
|
|
|
431
485
|
});
|
|
432
486
|
|
|
433
487
|
members
|
|
434
|
-
.command("revoke <
|
|
435
|
-
.description(
|
|
436
|
-
|
|
488
|
+
.command("revoke <target>")
|
|
489
|
+
.description(
|
|
490
|
+
"Revoke a pending invite. Accepts an email, personUid, full membershipKey, or legacy inviteToken.",
|
|
491
|
+
)
|
|
492
|
+
.action(async (target: string) => {
|
|
437
493
|
try {
|
|
438
494
|
const token = await ensureCognitoToken();
|
|
439
495
|
const companySlug = members.opts().company as string | undefined;
|
|
440
496
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
441
497
|
|
|
442
|
-
|
|
443
|
-
|
|
498
|
+
const membershipKey = resolveRevokeTargetToMembershipKey(
|
|
499
|
+
target,
|
|
500
|
+
companyUid,
|
|
501
|
+
);
|
|
502
|
+
await revokeInvite(token, membershipKey, companyUid);
|
|
503
|
+
console.log(chalk.green(`Revoked invite '${membershipKey}'`));
|
|
444
504
|
} catch (err) {
|
|
445
505
|
if (err instanceof InviteHttpError) {
|
|
446
506
|
const msg =
|