@indigoai-us/hq-cli 5.50.2 → 5.52.0
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/dist/bin/hq-auth-refresh.d.ts +1 -1
- package/dist/bin/hq-auth-refresh.js +5 -2
- package/dist/commands/files.js +33 -3
- package/dist/commands/members.d.ts +27 -0
- package/dist/commands/members.js +95 -37
- package/dist/commands/people.d.ts +26 -1
- package/dist/commands/people.js +70 -7
- package/dist/commands/secrets-scope.d.ts +20 -0
- package/dist/commands/secrets-scope.js +19 -0
- package/dist/commands/secrets.js +21 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +44 -14
- package/dist/node-preflight.d.ts +39 -0
- package/dist/node-preflight.js +55 -0
- package/dist/sentry.d.ts +12 -0
- package/dist/sentry.js +19 -3
- package/dist/utils/epipe.d.ts +8 -0
- package/dist/utils/epipe.js +30 -0
- package/dist/utils/intercepted-process-exit.d.ts +7 -0
- package/dist/utils/intercepted-process-exit.js +38 -0
- package/e2e/cli.test.ts +35 -0
- package/package.json +1 -1
- package/src/bin/hq-auth-refresh.ts +3 -0
- package/src/commands/files.test.ts +130 -0
- package/src/commands/files.ts +55 -3
- package/src/commands/members.test.ts +292 -0
- package/src/commands/members.ts +153 -43
- package/src/commands/people.test.ts +212 -5
- package/src/commands/people.ts +141 -5
- package/src/commands/secrets-scope.test.ts +56 -0
- package/src/commands/secrets-scope.ts +32 -0
- package/src/commands/secrets.ts +24 -10
- package/src/index.ts +40 -12
- package/src/node-preflight.test.ts +60 -0
- package/src/node-preflight.ts +67 -0
- package/src/sentry-epipe.test.ts +37 -0
- package/src/sentry-release.test.ts +54 -0
- package/src/sentry.ts +21 -1
- package/src/utils/epipe.test.ts +28 -0
- package/src/utils/epipe.ts +29 -0
- package/src/utils/intercepted-process-exit.test.ts +37 -0
- package/src/utils/intercepted-process-exit.ts +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.52.0]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`hq members promote <target> <newRole>` (alias `set-role`) — change a
|
|
10
|
+
member's role from the CLI.** A discoverable runner for role changes via
|
|
11
|
+
`POST /membership/role`, replacing the undiscoverable, `admin|member`-only
|
|
12
|
+
`set-role` (now kept as an alias). Supports the full role set
|
|
13
|
+
(`owner|admin|member|guest`); `<target>` may be an email, a `prs_` personUid,
|
|
14
|
+
or a full membership key (same resolver as `revoke`). It is a GENERAL role
|
|
15
|
+
change — it can promote OR demote — and authorization (owner-or-admin; only
|
|
16
|
+
an owner may set a target to owner or change an owner) is enforced
|
|
17
|
+
server-side.
|
|
18
|
+
- **`hq files share --full` — glob-safe whole-vault access.** Granting prefixes
|
|
19
|
+
one at a time hit `PolicyBudgetExceeded` after a few (each prefix is a
|
|
20
|
+
distinct ARN in the member's DEFLATE-packed inline STS session policy), and
|
|
21
|
+
the intended single-`*` wildcard escape hatch was unreachable because an
|
|
22
|
+
unquoted `*` expands to local filenames and fails the one-prefix check.
|
|
23
|
+
`--full` is a glob-safe flag that performs the coalesced whole-vault wildcard
|
|
24
|
+
grant in a single policy entry.
|
|
25
|
+
|
|
26
|
+
## [5.51.0]
|
|
27
|
+
|
|
5
28
|
### Fixed
|
|
6
29
|
|
|
7
30
|
- **`hq secrets exec` / `hq secrets env` load via the batch endpoint, killing
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
* machine-token mint). Exit 1 if no valid session could be ensured
|
|
11
11
|
* non-interactively.
|
|
12
12
|
*/
|
|
13
|
+
// MUST be first: guard the Node version before any dependency that needs a
|
|
14
|
+
// Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
|
|
13
15
|
|
|
14
|
-
!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]="
|
|
16
|
+
!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]="3c7e1b4e-66dc-5fe8-b828-cd3636df1fbb")}catch(e){}}();
|
|
17
|
+
import "../node-preflight.js";
|
|
15
18
|
import { initSentry, Sentry } from "../sentry.js";
|
|
16
19
|
import { refreshCachedSession } from "../utils/cognito-session.js";
|
|
17
20
|
initSentry();
|
|
@@ -38,4 +41,4 @@ initSentry();
|
|
|
38
41
|
}
|
|
39
42
|
})();
|
|
40
43
|
//# sourceMappingURL=hq-auth-refresh.js.map
|
|
41
|
-
//# debugId=
|
|
44
|
+
//# debugId=3c7e1b4e-66dc-5fe8-b828-cd3636df1fbb
|
package/dist/commands/files.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]="fc999359-c426-5277-ba96-5452ae1ed2e2")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import open from "open";
|
|
5
5
|
import * as readline from "node:readline";
|
|
@@ -99,10 +99,35 @@ export function registerFilesCommand(program) {
|
|
|
99
99
|
.description("Share file paths. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.")
|
|
100
100
|
.option("--with <principal>", "Email address, group id, or '@all' to share with every active company member")
|
|
101
101
|
.option("--permission <level>", "Permission level (only with --with): read | write")
|
|
102
|
+
.option("--full", "Grant access to the ENTIRE vault (the '*' wildcard prefix) — no need to quote a glob. Requires --with; defaults to write permission.")
|
|
102
103
|
.option("--expires <duration>", "Token expiry duration for share-session URL (e.g. 15m, 1h, 24h). Default 15m. Max 24h.")
|
|
103
104
|
.option("--no-open", "Print the share-session URL but do not launch the browser")
|
|
104
105
|
.action(async (paths, opts) => {
|
|
105
106
|
try {
|
|
107
|
+
// Full-vault grant: a glob-safe affordance for "give this person the
|
|
108
|
+
// whole vault" so admins never have to quote a `*` (an unquoted glob
|
|
109
|
+
// expands to local filenames and instantly fails the one-prefix
|
|
110
|
+
// check). Maps to the single `*` wildcard grant, which the server
|
|
111
|
+
// coalesces to one policy entry — sidestepping the per-prefix STS
|
|
112
|
+
// session-policy budget. Defaults to write permission.
|
|
113
|
+
if (opts.full) {
|
|
114
|
+
if (opts.with === undefined) {
|
|
115
|
+
console.error(chalk.red("--full grants whole-vault access to a principal and requires --with <principal>."));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
if (paths && paths.length > 0) {
|
|
119
|
+
console.error(chalk.red("--full grants the entire vault; do not also pass file paths."));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
await runDirectGrant({
|
|
123
|
+
prefix: "*",
|
|
124
|
+
principal: opts.with,
|
|
125
|
+
permission: opts.permission ?? "write",
|
|
126
|
+
companySlug: files.opts().company,
|
|
127
|
+
fullVault: true,
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
106
131
|
if (!paths || paths.length === 0) {
|
|
107
132
|
console.error(chalk.red("usage: hq files share <paths...> [--with <principal>]"));
|
|
108
133
|
process.exit(1);
|
|
@@ -437,7 +462,12 @@ async function runDirectGrant(params) {
|
|
|
437
462
|
const data = (await res.json());
|
|
438
463
|
const printedPrefix = data.acl?.path ?? data.acl?.prefix ?? canonicalPrefix;
|
|
439
464
|
const verb = autoCreated ? "Created ACL and granted" : "Granted";
|
|
440
|
-
|
|
465
|
+
if (params.fullVault) {
|
|
466
|
+
console.log(chalk.green(`${verb} ${params.permission} on the ENTIRE vault to ${principalLabel}`));
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
console.log(chalk.green(`${verb} ${params.permission} on ${printedPrefix} to ${principalLabel}`));
|
|
470
|
+
}
|
|
441
471
|
}
|
|
442
472
|
async function runShareSession(params) {
|
|
443
473
|
// Normalize every path through the shared prefix helper so a trailing
|
|
@@ -682,4 +712,4 @@ export async function runFilesDelete(params, deps = {}) {
|
|
|
682
712
|
}
|
|
683
713
|
}
|
|
684
714
|
//# sourceMappingURL=files.js.map
|
|
685
|
-
//# debugId=
|
|
715
|
+
//# debugId=fc999359-c426-5277-ba96-5452ae1ed2e2
|
|
@@ -12,6 +12,22 @@ export interface PendingInvite {
|
|
|
12
12
|
invitedBy: string;
|
|
13
13
|
invitedAt: string;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* An ACTIVE member of a company, as returned by
|
|
17
|
+
* `GET /membership/company/{companyUid}`. The server filters to
|
|
18
|
+
* `status: "active"` and enriches each row with resolved person metadata
|
|
19
|
+
* (`personEmail` / `personName` / `personSlug`) when available.
|
|
20
|
+
*/
|
|
21
|
+
export interface ActiveMember {
|
|
22
|
+
membershipKey: string;
|
|
23
|
+
personUid: string;
|
|
24
|
+
companyUid: string;
|
|
25
|
+
role: string;
|
|
26
|
+
status: string;
|
|
27
|
+
personEmail?: string;
|
|
28
|
+
personName?: string;
|
|
29
|
+
personSlug?: string;
|
|
30
|
+
}
|
|
15
31
|
export interface InviteOptions {
|
|
16
32
|
target: string;
|
|
17
33
|
role: string;
|
|
@@ -114,6 +130,7 @@ export declare class InviteHttpError extends Error {
|
|
|
114
130
|
}
|
|
115
131
|
export declare function formatInviteHttpError(status: number, fallback: string, code?: string): string;
|
|
116
132
|
export declare function listPendingInvites(token: string, companyUid: string): Promise<PendingInvite[]>;
|
|
133
|
+
export declare function listActiveMembers(token: string, companyUid: string): Promise<ActiveMember[]>;
|
|
117
134
|
/**
|
|
118
135
|
* Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
|
|
119
136
|
* the server requires. Accepts three input forms:
|
|
@@ -132,5 +149,15 @@ export declare function listPendingInvites(token: string, companyUid: string): P
|
|
|
132
149
|
*/
|
|
133
150
|
export declare function resolveRevokeTargetToMembershipKey(arg: string, companyUid: string): string;
|
|
134
151
|
export declare function revokeInvite(token: string, tokenOrKey: string, companyUid: string): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Change a member's role via `POST /membership/role`, accepting the FULL role
|
|
154
|
+
* set (owner|admin|member|guest). This is a GENERAL role change — it can promote
|
|
155
|
+
* OR demote. Authorization (owner-or-admin `changeRoles`, owner-only
|
|
156
|
+
* promote-to-owner / change-an-owner) is enforced SERVER-side; this function
|
|
157
|
+
* only validates the role string locally and surfaces the server's error.
|
|
158
|
+
* Role string is validated BEFORE any network call so callers/tests can rely on
|
|
159
|
+
* a synchronous-shaped rejection for a bad role.
|
|
160
|
+
*/
|
|
161
|
+
export declare function changeMemberRole(token: string, companyUid: string, membershipKey: string, newRole: Role): Promise<void>;
|
|
135
162
|
export declare function registerMembersCommand(program: Command): void;
|
|
136
163
|
//# sourceMappingURL=members.d.ts.map
|
package/dist/commands/members.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
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]="83a54ed2-3f52-5b6f-a784-09f78f488ddd")}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";
|
|
6
6
|
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
7
7
|
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
8
8
|
export const VALID_ROLES = new Set(["owner", "admin", "member", "guest"]);
|
|
9
|
-
const VALID_MEMBER_SET_ROLES = new Set(["admin", "member"]);
|
|
10
9
|
export function detectTarget(target) {
|
|
11
10
|
if (EMAIL_PATTERN.test(target)) {
|
|
12
11
|
return { type: "email", value: target.trim().toLowerCase() };
|
|
@@ -196,6 +195,20 @@ export async function listPendingInvites(token, companyUid) {
|
|
|
196
195
|
const data = (await res.json());
|
|
197
196
|
return data?.pending ?? data?.invites ?? [];
|
|
198
197
|
}
|
|
198
|
+
export async function listActiveMembers(token, companyUid) {
|
|
199
|
+
const res = await vaultApiFetch({
|
|
200
|
+
token,
|
|
201
|
+
path: `/membership/company/${encodeURIComponent(companyUid)}`,
|
|
202
|
+
});
|
|
203
|
+
if (!res.ok) {
|
|
204
|
+
const err = (await res.json().catch(() => ({})));
|
|
205
|
+
throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
206
|
+
}
|
|
207
|
+
// Server schema: `{ members: [...] }` — active members only, enriched with
|
|
208
|
+
// resolved person metadata (personEmail / personName / personSlug).
|
|
209
|
+
const data = (await res.json());
|
|
210
|
+
return data?.members ?? [];
|
|
211
|
+
}
|
|
199
212
|
/**
|
|
200
213
|
* Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
|
|
201
214
|
* the server requires. Accepts three input forms:
|
|
@@ -251,9 +264,18 @@ export async function revokeInvite(token, tokenOrKey, companyUid) {
|
|
|
251
264
|
throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
252
265
|
}
|
|
253
266
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Change a member's role via `POST /membership/role`, accepting the FULL role
|
|
269
|
+
* set (owner|admin|member|guest). This is a GENERAL role change — it can promote
|
|
270
|
+
* OR demote. Authorization (owner-or-admin `changeRoles`, owner-only
|
|
271
|
+
* promote-to-owner / change-an-owner) is enforced SERVER-side; this function
|
|
272
|
+
* only validates the role string locally and surfaces the server's error.
|
|
273
|
+
* Role string is validated BEFORE any network call so callers/tests can rely on
|
|
274
|
+
* a synchronous-shaped rejection for a bad role.
|
|
275
|
+
*/
|
|
276
|
+
export async function changeMemberRole(token, companyUid, membershipKey, newRole) {
|
|
277
|
+
if (!VALID_ROLES.has(newRole)) {
|
|
278
|
+
throw new Error(`Invalid role '${newRole}': must be one of owner, admin, member, guest`);
|
|
257
279
|
}
|
|
258
280
|
const res = await vaultApiFetch({
|
|
259
281
|
token,
|
|
@@ -271,16 +293,29 @@ export function registerMembersCommand(program) {
|
|
|
271
293
|
.command("members")
|
|
272
294
|
.description("Manage company memberships and invites")
|
|
273
295
|
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
296
|
+
// Canonical role-change command. `promote` is the discoverable verb users
|
|
297
|
+
// reach for; `set-role` is kept as an alias for the older name. The route is a
|
|
298
|
+
// GENERAL role change, so the help text and success message are honest that it
|
|
299
|
+
// can demote as well as promote. Authorization is enforced server-side.
|
|
274
300
|
members
|
|
275
|
-
.command("
|
|
276
|
-
.
|
|
277
|
-
.
|
|
301
|
+
.command("promote <target> <newRole>")
|
|
302
|
+
.alias("set-role")
|
|
303
|
+
.description("Change a member's role (owner|admin|member|guest) — promotes OR demotes. " +
|
|
304
|
+
"<target> may be an email, a prs_ personUid, or a full membership key. " +
|
|
305
|
+
"Owner-or-admin only; setting a target to owner (or changing an owner) is owner-only. Server-enforced.")
|
|
306
|
+
.action(async (target, newRole) => {
|
|
278
307
|
try {
|
|
308
|
+
const role = newRole.trim().toLowerCase();
|
|
309
|
+
if (!VALID_ROLES.has(role)) {
|
|
310
|
+
console.error(chalk.red(`Invalid role '${newRole}': must be one of owner, admin, member, guest`));
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
279
313
|
const token = await ensureCognitoToken();
|
|
280
314
|
const companySlug = members.opts().company;
|
|
281
315
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
282
|
-
|
|
283
|
-
|
|
316
|
+
const membershipKey = resolveRevokeTargetToMembershipKey(target, companyUid);
|
|
317
|
+
await changeMemberRole(token, companyUid, membershipKey, role);
|
|
318
|
+
console.log(chalk.green(`Updated role for '${target}' to ${role}`));
|
|
284
319
|
}
|
|
285
320
|
catch (err) {
|
|
286
321
|
if (err instanceof InviteHttpError) {
|
|
@@ -422,43 +457,66 @@ export function registerMembersCommand(program) {
|
|
|
422
457
|
});
|
|
423
458
|
members
|
|
424
459
|
.command("list")
|
|
425
|
-
.description("List pending
|
|
426
|
-
.
|
|
460
|
+
.description("List the company's active members (use --pending for pending invites)")
|
|
461
|
+
.option("--pending", "List pending invites instead of active members")
|
|
462
|
+
.action(async (opts) => {
|
|
427
463
|
try {
|
|
428
464
|
const token = await ensureCognitoToken();
|
|
429
465
|
const companySlug = members.opts().company;
|
|
430
466
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
467
|
+
if (opts.pending) {
|
|
468
|
+
// --pending: preserve the original pending-invites view verbatim.
|
|
469
|
+
const invites = await listPendingInvites(token, companyUid);
|
|
470
|
+
if (invites.length === 0) {
|
|
471
|
+
console.log(chalk.gray("No pending invites for this company."));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const targetW = Math.max(6, ...invites.map((i) => (i.inviteeEmail ?? i.personUid ?? "").length));
|
|
475
|
+
const roleW = Math.max(4, ...invites.map((i) => i.role.length));
|
|
476
|
+
const byW = Math.max(10, ...invites.map((i) => i.invitedBy.length));
|
|
477
|
+
const keyW = Math.max(14, ...invites.map((i) => i.membershipKey.length));
|
|
478
|
+
console.log(chalk.bold([
|
|
479
|
+
"TARGET".padEnd(targetW),
|
|
480
|
+
"ROLE".padEnd(roleW),
|
|
481
|
+
"INVITED_BY".padEnd(byW),
|
|
482
|
+
"INVITED_AT",
|
|
483
|
+
"MEMBERSHIP_KEY".padEnd(keyW),
|
|
484
|
+
].join(" ")));
|
|
485
|
+
for (const inv of invites) {
|
|
486
|
+
const target = inv.inviteeEmail ?? inv.personUid ?? "";
|
|
487
|
+
console.log([
|
|
488
|
+
target.padEnd(targetW),
|
|
489
|
+
inv.role.padEnd(roleW),
|
|
490
|
+
inv.invitedBy.padEnd(byW),
|
|
491
|
+
shortDate(inv.invitedAt),
|
|
492
|
+
inv.membershipKey.padEnd(keyW),
|
|
493
|
+
].join(" "));
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Default: list ACTIVE members so their emails drop straight into
|
|
498
|
+
// `hq secrets share <path> --with <email>`.
|
|
499
|
+
const activeMembers = await listActiveMembers(token, companyUid);
|
|
500
|
+
if (activeMembers.length === 0) {
|
|
501
|
+
console.log(chalk.gray("No active members found for this company."));
|
|
434
502
|
return;
|
|
435
503
|
}
|
|
436
|
-
const
|
|
437
|
-
const roleW = Math.max(4, ...
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
"
|
|
442
|
-
|
|
443
|
-
"INVITED_BY".padEnd(byW),
|
|
444
|
-
"INVITED_AT",
|
|
445
|
-
"MEMBERSHIP_KEY".padEnd(keyW),
|
|
446
|
-
].join(" ")));
|
|
447
|
-
for (const inv of invites) {
|
|
448
|
-
const target = inv.inviteeEmail ?? inv.personUid ?? "";
|
|
449
|
-
console.log([
|
|
450
|
-
target.padEnd(targetW),
|
|
451
|
-
inv.role.padEnd(roleW),
|
|
452
|
-
inv.invitedBy.padEnd(byW),
|
|
453
|
-
shortDate(inv.invitedAt),
|
|
454
|
-
inv.membershipKey.padEnd(keyW),
|
|
455
|
-
].join(" "));
|
|
504
|
+
const emailW = Math.max(5, ...activeMembers.map((m) => (m.personEmail ?? m.personUid).length));
|
|
505
|
+
const roleW = Math.max(4, ...activeMembers.map((m) => m.role.length));
|
|
506
|
+
console.log(chalk.bold(["EMAIL".padEnd(emailW), "ROLE".padEnd(roleW), "NAME"].join(" ")));
|
|
507
|
+
for (const m of activeMembers) {
|
|
508
|
+
const email = m.personEmail ?? m.personUid;
|
|
509
|
+
const name = m.personName ?? m.personSlug ?? "";
|
|
510
|
+
console.log([email.padEnd(emailW), m.role.padEnd(roleW), name].join(" "));
|
|
456
511
|
}
|
|
512
|
+
console.log(chalk.gray("Share a secret with a member: hq secrets share <path> --with <email>"));
|
|
457
513
|
}
|
|
458
514
|
catch (err) {
|
|
459
515
|
if (err instanceof InviteHttpError) {
|
|
460
516
|
const msg = err.status === 403
|
|
461
|
-
?
|
|
517
|
+
? opts.pending
|
|
518
|
+
? "Not authorized — only admins and owners can list invites"
|
|
519
|
+
: "Not authorized — only company members can list members"
|
|
462
520
|
: formatInviteHttpError(err.status, err.message);
|
|
463
521
|
console.error(chalk.red(msg));
|
|
464
522
|
process.exit(1);
|
|
@@ -497,4 +555,4 @@ export function registerMembersCommand(program) {
|
|
|
497
555
|
});
|
|
498
556
|
}
|
|
499
557
|
//# sourceMappingURL=members.js.map
|
|
500
|
-
//# debugId=
|
|
558
|
+
//# debugId=83a54ed2-3f52-5b6f-a784-09f78f488ddd
|
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
* named by `--company`); nothing reads across company boundaries.
|
|
12
12
|
*/
|
|
13
13
|
import { Command } from "commander";
|
|
14
|
+
import { resolveNameToEmail, type PersonRecord } from "../utils/people.js";
|
|
15
|
+
export type RefreshPeopleRoster = (hqRoot: string, companySlug: string) => Promise<void>;
|
|
16
|
+
interface PeopleCommandDeps {
|
|
17
|
+
refreshRoster?: RefreshPeopleRoster;
|
|
18
|
+
}
|
|
19
|
+
interface PeopleLookupOpts {
|
|
20
|
+
localOnly?: boolean;
|
|
21
|
+
json?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function refreshPeopleRosterFromCloud(hqRoot: string, companySlug: string): Promise<void>;
|
|
14
24
|
/**
|
|
15
25
|
* Resolve the single company to operate on. Explicit `--company` always wins
|
|
16
26
|
* (after a path-safety check). Otherwise the active company is inferred from
|
|
@@ -18,5 +28,20 @@ import { Command } from "commander";
|
|
|
18
28
|
* do, the caller must disambiguate with `--company`.
|
|
19
29
|
*/
|
|
20
30
|
export declare function resolveCompanySlug(hqRoot: string, explicit: string | undefined): string;
|
|
21
|
-
export declare function
|
|
31
|
+
export declare function resolvePersonWithRosterFallback(input: {
|
|
32
|
+
hqRoot: string;
|
|
33
|
+
slug: string;
|
|
34
|
+
name: string;
|
|
35
|
+
opts?: PeopleLookupOpts;
|
|
36
|
+
refreshRoster?: RefreshPeopleRoster;
|
|
37
|
+
}): Promise<ReturnType<typeof resolveNameToEmail>>;
|
|
38
|
+
export declare function searchPeopleWithRosterFallback(input: {
|
|
39
|
+
hqRoot: string;
|
|
40
|
+
slug: string;
|
|
41
|
+
keyword: string;
|
|
42
|
+
opts?: PeopleLookupOpts;
|
|
43
|
+
refreshRoster?: RefreshPeopleRoster;
|
|
44
|
+
}): Promise<PersonRecord[]>;
|
|
45
|
+
export declare function registerPeopleCommand(program: Command, deps?: PeopleCommandDeps): void;
|
|
46
|
+
export {};
|
|
22
47
|
//# sourceMappingURL=people.d.ts.map
|
package/dist/commands/people.js
CHANGED
|
@@ -11,14 +11,31 @@
|
|
|
11
11
|
* named by `--company`); nothing reads across company boundaries.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
!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]="
|
|
14
|
+
!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]="79abc1f5-4f5a-5c95-b29b-10f04417355f")}catch(e){}}();
|
|
15
15
|
import * as fs from "fs";
|
|
16
16
|
import { Option } from "commander";
|
|
17
17
|
import chalk from "chalk";
|
|
18
|
+
import { VaultClient } from "@indigoai-us/hq-cloud";
|
|
18
19
|
import * as yaml from "js-yaml";
|
|
19
20
|
import { findHqRoot } from "../utils/manifest.js";
|
|
20
21
|
import { manifestPath } from "./cloud-provision.js";
|
|
22
|
+
import { DEFAULT_COGNITO, buildVaultConfig, ensureCognitoToken, } from "../utils/cognito-session.js";
|
|
23
|
+
import { getCompanyUid } from "../utils/vault-api.js";
|
|
24
|
+
import { createCompanyPresignClient, runGet } from "./files-browse.js";
|
|
21
25
|
import { assertSafeCompanySlug, listCompanyPeople, searchPeople, resolveNameToEmail, companyPeopleDir, } from "../utils/people.js";
|
|
26
|
+
export async function refreshPeopleRosterFromCloud(hqRoot, companySlug) {
|
|
27
|
+
const accessToken = await ensureCognitoToken();
|
|
28
|
+
const client = new VaultClient(buildVaultConfig(accessToken));
|
|
29
|
+
await getCompanyUid(accessToken, companySlug);
|
|
30
|
+
await runGet({
|
|
31
|
+
path: `companies/${companySlug}/people/`,
|
|
32
|
+
hqRoot,
|
|
33
|
+
companySlug,
|
|
34
|
+
vaultClient: client,
|
|
35
|
+
companyClient: ({ companyUid }) => createCompanyPresignClient({ token: accessToken, companyUid }),
|
|
36
|
+
region: DEFAULT_COGNITO.region,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
22
39
|
/** Companies that still exist (anything not explicitly `status: archived`). */
|
|
23
40
|
function activeCompanySlugs(manifest) {
|
|
24
41
|
const companies = manifest.companies ?? {};
|
|
@@ -86,7 +103,39 @@ function fail(message) {
|
|
|
86
103
|
console.error(chalk.red(message));
|
|
87
104
|
process.exit(1);
|
|
88
105
|
}
|
|
89
|
-
|
|
106
|
+
function logRefreshFailure(companySlug, err) {
|
|
107
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
108
|
+
console.error(chalk.dim(` Could not refresh people roster for '${companySlug}': ${message}`));
|
|
109
|
+
}
|
|
110
|
+
async function tryRefreshRoster(refreshRoster, hqRoot, slug) {
|
|
111
|
+
try {
|
|
112
|
+
await refreshRoster(hqRoot, slug);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
logRefreshFailure(slug, err);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export async function resolvePersonWithRosterFallback(input) {
|
|
121
|
+
const local = resolveNameToEmail(listCompanyPeople(input.hqRoot, input.slug), input.name);
|
|
122
|
+
if (local.status !== "not_found" || input.opts?.localOnly)
|
|
123
|
+
return local;
|
|
124
|
+
const refreshed = await tryRefreshRoster(input.refreshRoster ?? refreshPeopleRosterFromCloud, input.hqRoot, input.slug);
|
|
125
|
+
if (!refreshed)
|
|
126
|
+
return local;
|
|
127
|
+
return resolveNameToEmail(listCompanyPeople(input.hqRoot, input.slug), input.name);
|
|
128
|
+
}
|
|
129
|
+
export async function searchPeopleWithRosterFallback(input) {
|
|
130
|
+
const local = searchPeople(listCompanyPeople(input.hqRoot, input.slug), input.keyword);
|
|
131
|
+
if (local.length > 0 || input.opts?.localOnly)
|
|
132
|
+
return local;
|
|
133
|
+
const refreshed = await tryRefreshRoster(input.refreshRoster ?? refreshPeopleRosterFromCloud, input.hqRoot, input.slug);
|
|
134
|
+
if (!refreshed)
|
|
135
|
+
return local;
|
|
136
|
+
return searchPeople(listCompanyPeople(input.hqRoot, input.slug), input.keyword);
|
|
137
|
+
}
|
|
138
|
+
export function registerPeopleCommand(program, deps = {}) {
|
|
90
139
|
const people = program
|
|
91
140
|
.command("people")
|
|
92
141
|
.description("List, search, and resolve a company's people (from companies/<co>/people)")
|
|
@@ -122,12 +171,19 @@ export function registerPeopleCommand(program) {
|
|
|
122
171
|
.command("search <keyword>")
|
|
123
172
|
.description("Keyword search over people names and emails")
|
|
124
173
|
.option("--json", "Output JSON instead of a table")
|
|
125
|
-
.
|
|
174
|
+
.option("--local-only", "Skip cloud fallback; search only the local people roster")
|
|
175
|
+
.action(async (keyword, opts) => {
|
|
126
176
|
try {
|
|
127
177
|
const scope = people.opts();
|
|
128
178
|
const hqRoot = resolveHqRoot(scope);
|
|
129
179
|
const slug = resolveCompanySlug(hqRoot, scope.company);
|
|
130
|
-
const matches =
|
|
180
|
+
const matches = await searchPeopleWithRosterFallback({
|
|
181
|
+
hqRoot,
|
|
182
|
+
slug,
|
|
183
|
+
keyword,
|
|
184
|
+
opts,
|
|
185
|
+
refreshRoster: deps.refreshRoster,
|
|
186
|
+
});
|
|
131
187
|
if (opts.json) {
|
|
132
188
|
console.log(JSON.stringify(matches, null, 2));
|
|
133
189
|
return;
|
|
@@ -146,12 +202,19 @@ export function registerPeopleCommand(program) {
|
|
|
146
202
|
.command("resolve <name>")
|
|
147
203
|
.description("Resolve a person name to their email address")
|
|
148
204
|
.option("--json", "Output JSON instead of plain text")
|
|
149
|
-
.
|
|
205
|
+
.option("--local-only", "Skip cloud fallback; resolve only from the local people roster")
|
|
206
|
+
.action(async (name, opts) => {
|
|
150
207
|
try {
|
|
151
208
|
const scope = people.opts();
|
|
152
209
|
const hqRoot = resolveHqRoot(scope);
|
|
153
210
|
const slug = resolveCompanySlug(hqRoot, scope.company);
|
|
154
|
-
const result =
|
|
211
|
+
const result = await resolvePersonWithRosterFallback({
|
|
212
|
+
hqRoot,
|
|
213
|
+
slug,
|
|
214
|
+
name,
|
|
215
|
+
opts,
|
|
216
|
+
refreshRoster: deps.refreshRoster,
|
|
217
|
+
});
|
|
155
218
|
if (opts.json) {
|
|
156
219
|
console.log(JSON.stringify(result, null, 2));
|
|
157
220
|
if (result.status === "found")
|
|
@@ -184,4 +247,4 @@ export function registerPeopleCommand(program) {
|
|
|
184
247
|
});
|
|
185
248
|
}
|
|
186
249
|
//# sourceMappingURL=people.js.map
|
|
187
|
-
//# debugId=
|
|
250
|
+
//# debugId=79abc1f5-4f5a-5c95-b29b-10f04417355f
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers describing WHICH secrets scope a command acted on, so `set` and
|
|
3
|
+
* `list` can echo it. Users were setting a secret in one scope (personal vs a
|
|
4
|
+
* company, or company A vs B) and listing another, then seeing "no secrets" with
|
|
5
|
+
* no indication the scopes differed (feedback_70e059da).
|
|
6
|
+
*/
|
|
7
|
+
export interface SecretsScopeRef {
|
|
8
|
+
/** True when --personal was used (caller's personal vault). */
|
|
9
|
+
personal: boolean;
|
|
10
|
+
/** The slug the user passed via --company, if any. */
|
|
11
|
+
companySlug?: string;
|
|
12
|
+
/** Resolved entity uid: prs_* for personal, cmp_* for a company. */
|
|
13
|
+
companyUid: string;
|
|
14
|
+
}
|
|
15
|
+
/** Human label for a secrets scope: "your personal vault" or "company <slug-or-uid>". */
|
|
16
|
+
export declare function describeSecretsScope(ref: SecretsScopeRef): string;
|
|
17
|
+
export declare function formatSecretSaved(name: string, scope: string): string;
|
|
18
|
+
export declare function formatSecretsListHeader(scope: string): string;
|
|
19
|
+
export declare function formatSecretsListEmpty(scope: string): string;
|
|
20
|
+
//# sourceMappingURL=secrets-scope.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Human label for a secrets scope: "your personal vault" or "company <slug-or-uid>". */
|
|
2
|
+
|
|
3
|
+
!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]="718913a3-f80f-5bff-928e-a7aa9f51dd0d")}catch(e){}}();
|
|
4
|
+
export function describeSecretsScope(ref) {
|
|
5
|
+
if (ref.personal)
|
|
6
|
+
return "your personal vault";
|
|
7
|
+
return `company ${ref.companySlug ?? ref.companyUid}`;
|
|
8
|
+
}
|
|
9
|
+
export function formatSecretSaved(name, scope) {
|
|
10
|
+
return `Secret '${name}' saved to ${scope}.`;
|
|
11
|
+
}
|
|
12
|
+
export function formatSecretsListHeader(scope) {
|
|
13
|
+
return `Secrets for ${scope}:`;
|
|
14
|
+
}
|
|
15
|
+
export function formatSecretsListEmpty(scope) {
|
|
16
|
+
return `No secrets found for ${scope}.`;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=secrets-scope.js.map
|
|
19
|
+
//# debugId=718913a3-f80f-5bff-928e-a7aa9f51dd0d
|
package/dist/commands/secrets.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]="96461f1a-6d5b-5dc0-b00a-0a9fa30a0dad")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import * as readline from "node:readline";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -8,6 +8,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
8
8
|
import { DEFAULT_SECRETS_CACHE_TTL_MS, readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
|
|
9
9
|
import { computeSha256 } from "../utils/integrity.js";
|
|
10
10
|
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN } from "./_patterns.js";
|
|
11
|
+
import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, formatSecretsListHeader, } from "./secrets-scope.js";
|
|
11
12
|
import { vaultApiFetch, getCompanyUid, getEntityUid, } from "../utils/vault-api.js";
|
|
12
13
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
13
14
|
function scopeOpts(opts) {
|
|
@@ -344,7 +345,13 @@ export function registerSecretsCommand(program) {
|
|
|
344
345
|
process.exit(1);
|
|
345
346
|
}
|
|
346
347
|
const token = await ensureCognitoToken();
|
|
347
|
-
const
|
|
348
|
+
const scope = scopeOpts(secrets.opts());
|
|
349
|
+
const companyUid = await getEntityUid(token, scope);
|
|
350
|
+
const scopeLabel = describeSecretsScope({
|
|
351
|
+
personal: scope.personal,
|
|
352
|
+
companySlug: scope.companySlug,
|
|
353
|
+
companyUid,
|
|
354
|
+
});
|
|
348
355
|
const res = await vaultApiFetch({
|
|
349
356
|
token,
|
|
350
357
|
path: `/secrets/${encodeURIComponent(companyUid)}`,
|
|
@@ -357,7 +364,7 @@ export function registerSecretsCommand(program) {
|
|
|
357
364
|
process.exit(1);
|
|
358
365
|
}
|
|
359
366
|
removeCacheEntry(companyUid, name);
|
|
360
|
-
console.log(chalk.green(
|
|
367
|
+
console.log(chalk.green(formatSecretSaved(name, scopeLabel)));
|
|
361
368
|
}
|
|
362
369
|
catch (err) {
|
|
363
370
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
@@ -487,7 +494,13 @@ export function registerSecretsCommand(program) {
|
|
|
487
494
|
normalizedPrefix = normalized;
|
|
488
495
|
}
|
|
489
496
|
const token = await ensureCognitoToken();
|
|
490
|
-
const
|
|
497
|
+
const scope = scopeOpts(secrets.opts());
|
|
498
|
+
const companyUid = await getEntityUid(token, scope);
|
|
499
|
+
const scopeLabel = describeSecretsScope({
|
|
500
|
+
personal: scope.personal,
|
|
501
|
+
companySlug: scope.companySlug,
|
|
502
|
+
companyUid,
|
|
503
|
+
});
|
|
491
504
|
const query = {};
|
|
492
505
|
if (normalizedPrefix) {
|
|
493
506
|
query.prefix = normalizedPrefix;
|
|
@@ -504,7 +517,7 @@ export function registerSecretsCommand(program) {
|
|
|
504
517
|
}
|
|
505
518
|
const data = (await res.json());
|
|
506
519
|
if (data.secrets.length === 0) {
|
|
507
|
-
console.log(chalk.dim(
|
|
520
|
+
console.log(chalk.dim(formatSecretsListEmpty(scopeLabel)));
|
|
508
521
|
return;
|
|
509
522
|
}
|
|
510
523
|
const nameWidth = Math.max(4, ...data.secrets.map((s) => s.name.length));
|
|
@@ -514,6 +527,7 @@ export function registerSecretsCommand(program) {
|
|
|
514
527
|
if (hasPermission) {
|
|
515
528
|
const accessWidth = Math.max(6, ...data.secrets.map((s) => (s.permission ?? "-").length));
|
|
516
529
|
const header = `${"NAME".padEnd(nameWidth)} ${"ACCESS".padEnd(accessWidth)} ${"TIER".padEnd(tierWidth)} ${"SCRIPT LOCK".padEnd(scriptLockWidth)} LAST MODIFIED`;
|
|
530
|
+
console.log(chalk.dim(formatSecretsListHeader(scopeLabel)));
|
|
517
531
|
console.log(chalk.bold(header));
|
|
518
532
|
for (const s of data.secrets) {
|
|
519
533
|
const access = s.permission ?? "-";
|
|
@@ -525,6 +539,7 @@ export function registerSecretsCommand(program) {
|
|
|
525
539
|
}
|
|
526
540
|
else {
|
|
527
541
|
const header = `${"NAME".padEnd(nameWidth)} ${"TIER".padEnd(tierWidth)} ${"SCRIPT LOCK".padEnd(scriptLockWidth)} LAST MODIFIED`;
|
|
542
|
+
console.log(chalk.dim(formatSecretsListHeader(scopeLabel)));
|
|
528
543
|
console.log(chalk.bold(header));
|
|
529
544
|
for (const s of data.secrets) {
|
|
530
545
|
const tier = normalizeSecretTier(s.tier);
|
|
@@ -1148,4 +1163,4 @@ export function registerSecretsCommand(program) {
|
|
|
1148
1163
|
});
|
|
1149
1164
|
}
|
|
1150
1165
|
//# sourceMappingURL=secrets.js.map
|
|
1151
|
-
//# debugId=
|
|
1166
|
+
//# debugId=96461f1a-6d5b-5dc0-b00a-0a9fa30a0dad
|