@indigoai-us/hq-cli 5.10.0 → 5.11.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/dist/commands/cloud.js +42 -3
- package/dist/commands/members.d.ts +53 -0
- package/dist/commands/members.js +240 -0
- package/dist/commands/secrets.d.ts +2 -2
- package/dist/commands/secrets.js +33 -25
- package/dist/index.js +5 -2
- package/dist/utils/vault-api.d.ts +5 -0
- package/dist/utils/vault-api.js +35 -2
- package/package.json +1 -1
- package/src/commands/cloud.ts +40 -0
- package/src/commands/members.test.ts +304 -0
- package/src/commands/members.ts +367 -0
- package/src/commands/secrets.ts +86 -23
- package/src/index.ts +4 -0
- package/src/utils/vault-api.test.ts +111 -0
- package/src/utils/vault-api.ts +43 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
4
|
+
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
5
|
+
|
|
6
|
+
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
7
|
+
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
8
|
+
export const VALID_ROLES = new Set(["owner", "admin", "member", "guest"]);
|
|
9
|
+
|
|
10
|
+
export type Role = "owner" | "admin" | "member" | "guest";
|
|
11
|
+
|
|
12
|
+
export interface PendingInvite {
|
|
13
|
+
membershipKey: string;
|
|
14
|
+
personUid?: string;
|
|
15
|
+
inviteeEmail?: string;
|
|
16
|
+
companyUid: string;
|
|
17
|
+
role: string;
|
|
18
|
+
status: string;
|
|
19
|
+
inviteToken?: string;
|
|
20
|
+
invitedBy: string;
|
|
21
|
+
invitedAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface MyMembership {
|
|
25
|
+
membershipKey: string;
|
|
26
|
+
personUid: string;
|
|
27
|
+
companyUid: string;
|
|
28
|
+
role: string;
|
|
29
|
+
status: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface InviteOptions {
|
|
33
|
+
target: string;
|
|
34
|
+
role: string;
|
|
35
|
+
paths?: string;
|
|
36
|
+
companyUid: string;
|
|
37
|
+
callerUid: string;
|
|
38
|
+
token: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface InviteResult {
|
|
42
|
+
inviteToken: string;
|
|
43
|
+
magicLink: string;
|
|
44
|
+
membership: { role: string; status: string };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DetectedTarget {
|
|
48
|
+
type: "email" | "person";
|
|
49
|
+
value: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function detectTarget(target: string): DetectedTarget | null {
|
|
53
|
+
if (EMAIL_PATTERN.test(target)) {
|
|
54
|
+
return { type: "email", value: target.trim().toLowerCase() };
|
|
55
|
+
}
|
|
56
|
+
if (PERSON_UID_PATTERN.test(target)) {
|
|
57
|
+
return { type: "person", value: target };
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function shortDate(iso: string): string {
|
|
63
|
+
return iso.slice(0, 10);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the caller's personUid by reading their own membership list.
|
|
68
|
+
* The server infers the JWT identity, so this returns the canonical
|
|
69
|
+
* personUid attached to the caller's active memberships.
|
|
70
|
+
*/
|
|
71
|
+
export async function getCallerPersonUid(token: string): Promise<string> {
|
|
72
|
+
const res = await vaultApiFetch({ token, path: "/membership/me" });
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"Failed to resolve caller identity — run `hq login` and try again",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const data = (await res.json()) as { memberships: MyMembership[] };
|
|
79
|
+
const personUid = data.memberships.find((m) => m.personUid)?.personUid;
|
|
80
|
+
if (!personUid) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
"Your account has no person entity yet. Run `hq onboard create-company` or accept an invite first.",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return personUid;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Send a `/membership/invite` request and return the magic link. */
|
|
89
|
+
export async function inviteMember(
|
|
90
|
+
options: InviteOptions,
|
|
91
|
+
): Promise<InviteResult> {
|
|
92
|
+
if (!VALID_ROLES.has(options.role)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Invalid role '${options.role}': must be one of owner, admin, member, guest`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (options.paths && options.role !== "guest") {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"--paths is only valid with --role guest (allowedPrefixes are only meaningful for the guest role)",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const detected = detectTarget(options.target);
|
|
104
|
+
if (!detected) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Invalid target '${options.target}': must be an email address or a personUid matching prs_<alphanumeric>`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const allowedPrefixes = options.paths
|
|
111
|
+
? options.paths.split(",").map((p) => p.trim()).filter(Boolean)
|
|
112
|
+
: undefined;
|
|
113
|
+
|
|
114
|
+
const body: Record<string, unknown> = {
|
|
115
|
+
companyUid: options.companyUid,
|
|
116
|
+
role: options.role,
|
|
117
|
+
invitedBy: options.callerUid,
|
|
118
|
+
};
|
|
119
|
+
if (detected.type === "email") body.inviteeEmail = detected.value;
|
|
120
|
+
else body.personUid = detected.value;
|
|
121
|
+
if (allowedPrefixes) body.allowedPrefixes = allowedPrefixes;
|
|
122
|
+
|
|
123
|
+
const res = await vaultApiFetch({
|
|
124
|
+
token: options.token,
|
|
125
|
+
path: "/membership/invite",
|
|
126
|
+
method: "POST",
|
|
127
|
+
body,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!res.ok) {
|
|
131
|
+
const err = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
132
|
+
throw new InviteHttpError(
|
|
133
|
+
res.status,
|
|
134
|
+
err.message ?? err.error ?? res.statusText,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const data = (await res.json()) as {
|
|
139
|
+
membership: { role: string; status: string };
|
|
140
|
+
inviteToken: string;
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
inviteToken: data.inviteToken,
|
|
144
|
+
magicLink: `hq://accept/${data.inviteToken}`,
|
|
145
|
+
membership: data.membership,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export class InviteHttpError extends Error {
|
|
150
|
+
constructor(public status: number, message: string) {
|
|
151
|
+
super(message);
|
|
152
|
+
this.name = "InviteHttpError";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatInviteHttpError(status: number, fallback: string): string {
|
|
157
|
+
if (status === 401) return "Not authenticated — please run `hq login`";
|
|
158
|
+
if (status === 403) {
|
|
159
|
+
return "Not authorized — only admins and owners can invite members";
|
|
160
|
+
}
|
|
161
|
+
if (status === 409) {
|
|
162
|
+
return "This person already has a membership or pending invite for this company";
|
|
163
|
+
}
|
|
164
|
+
if (status >= 500) return `Server error: ${fallback}`;
|
|
165
|
+
return fallback;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function listPendingInvites(
|
|
169
|
+
token: string,
|
|
170
|
+
companyUid: string,
|
|
171
|
+
): Promise<PendingInvite[]> {
|
|
172
|
+
const res = await vaultApiFetch({
|
|
173
|
+
token,
|
|
174
|
+
path: `/membership/company/${encodeURIComponent(companyUid)}/pending`,
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const err = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
178
|
+
throw new InviteHttpError(
|
|
179
|
+
res.status,
|
|
180
|
+
err.message ?? err.error ?? res.statusText,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const data = (await res.json()) as { invites: PendingInvite[] };
|
|
184
|
+
return data.invites;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function revokeInvite(
|
|
188
|
+
token: string,
|
|
189
|
+
tokenOrKey: string,
|
|
190
|
+
companyUid: string,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
const res = await vaultApiFetch({
|
|
193
|
+
token,
|
|
194
|
+
path: "/membership/revoke",
|
|
195
|
+
method: "POST",
|
|
196
|
+
body: { membershipKey: tokenOrKey, companyUid },
|
|
197
|
+
});
|
|
198
|
+
if (!res.ok) {
|
|
199
|
+
const err = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
200
|
+
throw new InviteHttpError(
|
|
201
|
+
res.status,
|
|
202
|
+
err.message ?? err.error ?? res.statusText,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function registerMembersCommand(program: Command): void {
|
|
208
|
+
const members = program
|
|
209
|
+
.command("members")
|
|
210
|
+
.description("Manage company memberships and invites")
|
|
211
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
212
|
+
|
|
213
|
+
members
|
|
214
|
+
.command("invite <target>")
|
|
215
|
+
.description(
|
|
216
|
+
"Invite a person to the company by email or personUid (prints a magic link)",
|
|
217
|
+
)
|
|
218
|
+
.option(
|
|
219
|
+
"--role <role>",
|
|
220
|
+
"Role for the invitee: owner, admin, member, or guest",
|
|
221
|
+
"member",
|
|
222
|
+
)
|
|
223
|
+
.option(
|
|
224
|
+
"--paths <prefixes>",
|
|
225
|
+
"Comma-separated allowed prefixes (only valid with --role guest)",
|
|
226
|
+
)
|
|
227
|
+
.action(
|
|
228
|
+
async (
|
|
229
|
+
target: string,
|
|
230
|
+
opts: { role: string; paths?: string },
|
|
231
|
+
) => {
|
|
232
|
+
try {
|
|
233
|
+
const token = await ensureCognitoToken();
|
|
234
|
+
const companySlug = members.opts().company as string | undefined;
|
|
235
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
236
|
+
const callerUid = await getCallerPersonUid(token);
|
|
237
|
+
|
|
238
|
+
const result = await inviteMember({
|
|
239
|
+
target,
|
|
240
|
+
role: opts.role,
|
|
241
|
+
paths: opts.paths,
|
|
242
|
+
companyUid,
|
|
243
|
+
callerUid,
|
|
244
|
+
token,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
console.log(
|
|
248
|
+
chalk.green(
|
|
249
|
+
`Invited ${target} as ${result.membership.role} (status: ${result.membership.status})`,
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
console.log();
|
|
253
|
+
console.log(chalk.bold("Magic link:"));
|
|
254
|
+
console.log(` ${result.magicLink}`);
|
|
255
|
+
console.log();
|
|
256
|
+
console.log(
|
|
257
|
+
chalk.dim(
|
|
258
|
+
"Share this link with the invitee. They can run `hq onboard join --invite-token <token>` to accept.",
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (err instanceof InviteHttpError) {
|
|
263
|
+
console.error(chalk.red(formatInviteHttpError(err.status, err.message)));
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
console.error(
|
|
267
|
+
chalk.red("Error:"),
|
|
268
|
+
err instanceof Error ? err.message : String(err),
|
|
269
|
+
);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
members
|
|
276
|
+
.command("list")
|
|
277
|
+
.description("List pending invites for the company")
|
|
278
|
+
.action(async () => {
|
|
279
|
+
try {
|
|
280
|
+
const token = await ensureCognitoToken();
|
|
281
|
+
const companySlug = members.opts().company as string | undefined;
|
|
282
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
283
|
+
|
|
284
|
+
const invites = await listPendingInvites(token, companyUid);
|
|
285
|
+
|
|
286
|
+
if (invites.length === 0) {
|
|
287
|
+
console.log(chalk.gray("No pending invites for this company."));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const targetW = Math.max(
|
|
292
|
+
6,
|
|
293
|
+
...invites.map((i) => (i.inviteeEmail ?? i.personUid ?? "").length),
|
|
294
|
+
);
|
|
295
|
+
const roleW = Math.max(4, ...invites.map((i) => i.role.length));
|
|
296
|
+
const byW = Math.max(10, ...invites.map((i) => i.invitedBy.length));
|
|
297
|
+
const keyW = Math.max(14, ...invites.map((i) => i.membershipKey.length));
|
|
298
|
+
console.log(
|
|
299
|
+
chalk.bold(
|
|
300
|
+
[
|
|
301
|
+
"TARGET".padEnd(targetW),
|
|
302
|
+
"ROLE".padEnd(roleW),
|
|
303
|
+
"INVITED_BY".padEnd(byW),
|
|
304
|
+
"INVITED_AT",
|
|
305
|
+
"MEMBERSHIP_KEY".padEnd(keyW),
|
|
306
|
+
].join(" "),
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
for (const inv of invites) {
|
|
310
|
+
const target = inv.inviteeEmail ?? inv.personUid ?? "";
|
|
311
|
+
console.log(
|
|
312
|
+
[
|
|
313
|
+
target.padEnd(targetW),
|
|
314
|
+
inv.role.padEnd(roleW),
|
|
315
|
+
inv.invitedBy.padEnd(byW),
|
|
316
|
+
shortDate(inv.invitedAt),
|
|
317
|
+
inv.membershipKey.padEnd(keyW),
|
|
318
|
+
].join(" "),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
} catch (err) {
|
|
322
|
+
if (err instanceof InviteHttpError) {
|
|
323
|
+
const msg =
|
|
324
|
+
err.status === 403
|
|
325
|
+
? "Not authorized — only admins and owners can list invites"
|
|
326
|
+
: formatInviteHttpError(err.status, err.message);
|
|
327
|
+
console.error(chalk.red(msg));
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
console.error(
|
|
331
|
+
chalk.red("Error:"),
|
|
332
|
+
err instanceof Error ? err.message : String(err),
|
|
333
|
+
);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
members
|
|
339
|
+
.command("revoke <tokenOrKey>")
|
|
340
|
+
.description("Revoke a pending invite (accepts the inviteToken or membershipKey)")
|
|
341
|
+
.action(async (tokenOrKey: string) => {
|
|
342
|
+
try {
|
|
343
|
+
const token = await ensureCognitoToken();
|
|
344
|
+
const companySlug = members.opts().company as string | undefined;
|
|
345
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
346
|
+
|
|
347
|
+
await revokeInvite(token, tokenOrKey, companyUid);
|
|
348
|
+
console.log(chalk.green(`Revoked invite '${tokenOrKey}'`));
|
|
349
|
+
} catch (err) {
|
|
350
|
+
if (err instanceof InviteHttpError) {
|
|
351
|
+
const msg =
|
|
352
|
+
err.status === 403
|
|
353
|
+
? "Not authorized — only admins and owners can revoke invites"
|
|
354
|
+
: err.status === 404
|
|
355
|
+
? "Invite not found — it may have already been accepted or revoked"
|
|
356
|
+
: formatInviteHttpError(err.status, err.message);
|
|
357
|
+
console.error(chalk.red(msg));
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
console.error(
|
|
361
|
+
chalk.red("Error:"),
|
|
362
|
+
err instanceof Error ? err.message : String(err),
|
|
363
|
+
);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
package/src/commands/secrets.ts
CHANGED
|
@@ -10,9 +10,40 @@ import {
|
|
|
10
10
|
clearAllCache,
|
|
11
11
|
} from "../utils/secrets-cache.js";
|
|
12
12
|
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN } from "./_patterns.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
vaultApiFetch,
|
|
15
|
+
getCompanyUid,
|
|
16
|
+
getEntityUid,
|
|
17
|
+
} from "../utils/vault-api.js";
|
|
14
18
|
export type { VaultApiOptions } from "../utils/vault-api.js";
|
|
15
|
-
export { vaultApiFetch, getCompanyUid };
|
|
19
|
+
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
20
|
+
|
|
21
|
+
interface SecretsScopeOpts {
|
|
22
|
+
company?: string;
|
|
23
|
+
personal?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function scopeOpts(opts: SecretsScopeOpts): {
|
|
27
|
+
personal: boolean;
|
|
28
|
+
companySlug: string | undefined;
|
|
29
|
+
} {
|
|
30
|
+
if (opts.personal && opts.company) {
|
|
31
|
+
console.error(
|
|
32
|
+
chalk.red("Error: --personal cannot be combined with --company."),
|
|
33
|
+
);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
return { personal: !!opts.personal, companySlug: opts.company };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rejectIfPersonal(opts: SecretsScopeOpts, action: string): void {
|
|
40
|
+
if (opts.personal) {
|
|
41
|
+
console.error(
|
|
42
|
+
chalk.red(`Error: ${action} is not supported with --personal.`),
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
16
47
|
|
|
17
48
|
function shellSingleQuote(value: string): string {
|
|
18
49
|
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
@@ -125,7 +156,11 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
125
156
|
const secrets = program
|
|
126
157
|
.command("secrets")
|
|
127
158
|
.description("Manage secrets in HQ vault (SSM Parameter Store)")
|
|
128
|
-
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
159
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
160
|
+
.option(
|
|
161
|
+
"--personal",
|
|
162
|
+
"Operate on the caller's personal vault (no sharing)",
|
|
163
|
+
);
|
|
129
164
|
|
|
130
165
|
secrets
|
|
131
166
|
.command("set <name>")
|
|
@@ -164,8 +199,10 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
164
199
|
}
|
|
165
200
|
|
|
166
201
|
const token = await ensureCognitoToken();
|
|
167
|
-
const
|
|
168
|
-
|
|
202
|
+
const companyUid = await getEntityUid(
|
|
203
|
+
token,
|
|
204
|
+
scopeOpts(secrets.opts()),
|
|
205
|
+
);
|
|
169
206
|
|
|
170
207
|
const res = await vaultApiFetch({
|
|
171
208
|
token,
|
|
@@ -200,8 +237,10 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
200
237
|
.action(async (name: string, opts: { reveal?: boolean }) => {
|
|
201
238
|
try {
|
|
202
239
|
const token = await ensureCognitoToken();
|
|
203
|
-
const
|
|
204
|
-
|
|
240
|
+
const companyUid = await getEntityUid(
|
|
241
|
+
token,
|
|
242
|
+
scopeOpts(secrets.opts()),
|
|
243
|
+
);
|
|
205
244
|
|
|
206
245
|
const query: Record<string, string> = {};
|
|
207
246
|
if (opts.reveal) {
|
|
@@ -283,8 +322,10 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
283
322
|
}
|
|
284
323
|
|
|
285
324
|
const token = await ensureCognitoToken();
|
|
286
|
-
const
|
|
287
|
-
|
|
325
|
+
const companyUid = await getEntityUid(
|
|
326
|
+
token,
|
|
327
|
+
scopeOpts(secrets.opts()),
|
|
328
|
+
);
|
|
288
329
|
|
|
289
330
|
const query: Record<string, string> = {};
|
|
290
331
|
if (normalizedPrefix) {
|
|
@@ -364,8 +405,10 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
364
405
|
}
|
|
365
406
|
|
|
366
407
|
const token = await ensureCognitoToken();
|
|
367
|
-
const
|
|
368
|
-
|
|
408
|
+
const companyUid = await getEntityUid(
|
|
409
|
+
token,
|
|
410
|
+
scopeOpts(secrets.opts()),
|
|
411
|
+
);
|
|
369
412
|
|
|
370
413
|
const res = await vaultApiFetch({
|
|
371
414
|
token,
|
|
@@ -427,8 +470,10 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
427
470
|
}
|
|
428
471
|
|
|
429
472
|
const token = await ensureCognitoToken();
|
|
430
|
-
const
|
|
431
|
-
|
|
473
|
+
const companyUid = await getEntityUid(
|
|
474
|
+
token,
|
|
475
|
+
scopeOpts(secrets.opts()),
|
|
476
|
+
);
|
|
432
477
|
|
|
433
478
|
const revealed = await Promise.all(
|
|
434
479
|
keys.map(async (key) => {
|
|
@@ -518,8 +563,10 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
518
563
|
}
|
|
519
564
|
|
|
520
565
|
const token = await ensureCognitoToken();
|
|
521
|
-
const
|
|
522
|
-
|
|
566
|
+
const companyUid = await getEntityUid(
|
|
567
|
+
token,
|
|
568
|
+
scopeOpts(secrets.opts()),
|
|
569
|
+
);
|
|
523
570
|
|
|
524
571
|
const revealed = await Promise.all(
|
|
525
572
|
keys.map(async (key) => {
|
|
@@ -568,6 +615,8 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
568
615
|
.option("--expires <duration>", "Token expiry duration (e.g. 24h, 2d, 30m)", "24h")
|
|
569
616
|
.action(async (name: string, opts: { expires: string }) => {
|
|
570
617
|
try {
|
|
618
|
+
rejectIfPersonal(secrets.opts(), "generate-link");
|
|
619
|
+
|
|
571
620
|
if (!SECRET_NAME_PATTERN.test(name)) {
|
|
572
621
|
console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
|
|
573
622
|
process.exit(1);
|
|
@@ -586,8 +635,10 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
586
635
|
}
|
|
587
636
|
|
|
588
637
|
const token = await ensureCognitoToken();
|
|
589
|
-
const
|
|
590
|
-
|
|
638
|
+
const companyUid = await getEntityUid(
|
|
639
|
+
token,
|
|
640
|
+
scopeOpts(secrets.opts()),
|
|
641
|
+
);
|
|
591
642
|
|
|
592
643
|
const res = await vaultApiFetch({
|
|
593
644
|
token,
|
|
@@ -632,6 +683,8 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
632
683
|
.requiredOption("--permission <level>", "Permission level: read | write | admin")
|
|
633
684
|
.action(async (path: string, opts: { with: string; permission: string }) => {
|
|
634
685
|
try {
|
|
686
|
+
rejectIfPersonal(secrets.opts(), "share");
|
|
687
|
+
|
|
635
688
|
if (!SECRET_NAME_PATTERN.test(path)) {
|
|
636
689
|
console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
|
|
637
690
|
process.exit(1);
|
|
@@ -651,8 +704,10 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
651
704
|
const granteeId = opts.with;
|
|
652
705
|
|
|
653
706
|
const token = await ensureCognitoToken();
|
|
654
|
-
const
|
|
655
|
-
|
|
707
|
+
const companyUid = await getEntityUid(
|
|
708
|
+
token,
|
|
709
|
+
scopeOpts(secrets.opts()),
|
|
710
|
+
);
|
|
656
711
|
|
|
657
712
|
const res = await vaultApiFetch({
|
|
658
713
|
token,
|
|
@@ -695,6 +750,8 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
695
750
|
.requiredOption("--from <principal>", "Email address or group id to remove")
|
|
696
751
|
.action(async (path: string, opts: { from: string }) => {
|
|
697
752
|
try {
|
|
753
|
+
rejectIfPersonal(secrets.opts(), "unshare");
|
|
754
|
+
|
|
698
755
|
if (!SECRET_NAME_PATTERN.test(path)) {
|
|
699
756
|
console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
|
|
700
757
|
process.exit(1);
|
|
@@ -709,8 +766,10 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
709
766
|
const granteeId = opts.from;
|
|
710
767
|
|
|
711
768
|
const token = await ensureCognitoToken();
|
|
712
|
-
const
|
|
713
|
-
|
|
769
|
+
const companyUid = await getEntityUid(
|
|
770
|
+
token,
|
|
771
|
+
scopeOpts(secrets.opts()),
|
|
772
|
+
);
|
|
714
773
|
|
|
715
774
|
const res = await vaultApiFetch({
|
|
716
775
|
token,
|
|
@@ -751,14 +810,18 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
751
810
|
.description("Show the ACL (access control list) for a secret path")
|
|
752
811
|
.action(async (path: string) => {
|
|
753
812
|
try {
|
|
813
|
+
rejectIfPersonal(secrets.opts(), "acl");
|
|
814
|
+
|
|
754
815
|
if (!SECRET_NAME_PATTERN.test(path)) {
|
|
755
816
|
console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
|
|
756
817
|
process.exit(1);
|
|
757
818
|
}
|
|
758
819
|
|
|
759
820
|
const token = await ensureCognitoToken();
|
|
760
|
-
const
|
|
761
|
-
|
|
821
|
+
const companyUid = await getEntityUid(
|
|
822
|
+
token,
|
|
823
|
+
scopeOpts(secrets.opts()),
|
|
824
|
+
);
|
|
762
825
|
|
|
763
826
|
const secretPath = path;
|
|
764
827
|
const res = await vaultApiFetch({
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { registerSecretsCommand } from "./commands/secrets.js";
|
|
|
27
27
|
import { registerRunCommand } from "./commands/run.js";
|
|
28
28
|
import { registerGroupsCommand } from "./commands/groups.js";
|
|
29
29
|
import { registerFilesCommand } from "./commands/files.js";
|
|
30
|
+
import { registerMembersCommand } from "./commands/members.js";
|
|
30
31
|
|
|
31
32
|
initSentry();
|
|
32
33
|
|
|
@@ -102,6 +103,9 @@ registerGroupsCommand(program);
|
|
|
102
103
|
// Files ACL management (subcommand group — hq files share|unshare|acl)
|
|
103
104
|
registerFilesCommand(program);
|
|
104
105
|
|
|
106
|
+
// Membership management (subcommand group — hq members invite|list|revoke)
|
|
107
|
+
registerMembersCommand(program);
|
|
108
|
+
|
|
105
109
|
// Onboarding (top-level — Cognito + vault-service provisioning)
|
|
106
110
|
registerOnboardCommand(program);
|
|
107
111
|
|